@codyswann/lisa 4.33.8 → 4.33.10

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 (92) hide show
  1. package/README.md +22 -0
  2. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -1
  3. package/all/copy-overwrite/scripts/lisa-work-item.mjs +361 -10
  4. package/dist/configs/vitest/base.d.ts +76 -11
  5. package/dist/configs/vitest/base.d.ts.map +1 -1
  6. package/dist/configs/vitest/base.js +121 -14
  7. package/dist/configs/vitest/base.js.map +1 -1
  8. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  9. package/dist/core/lisa-owned-hash-ledger.js +12 -0
  10. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  11. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  12. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  13. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  14. package/dist/core/upstream-evidence-manifest.js +12 -9
  15. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  16. package/dist/migrations/ensure-lisa-postinstall.d.ts +26 -15
  17. package/dist/migrations/ensure-lisa-postinstall.d.ts.map +1 -1
  18. package/dist/migrations/ensure-lisa-postinstall.js +43 -20
  19. package/dist/migrations/ensure-lisa-postinstall.js.map +1 -1
  20. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -1
  21. package/package.json +4 -4
  22. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  24. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  25. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +1 -1
  26. package/plugins/lisa/hooks/parity-safety-net.sh +44 -1
  27. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  28. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +1 -1
  29. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -1
  30. package/plugins/lisa-agy/plugin.json +1 -1
  31. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  32. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +1 -1
  33. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  36. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -1
  40. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  41. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +1 -1
  42. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -1
  44. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  45. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +1 -1
  46. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-expo-agy/plugin.json +1 -1
  49. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  54. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  59. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  63. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  64. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  69. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-rails-agy/plugin.json +1 -1
  74. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  79. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  84. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/src/base/hooks/parity-safety-net.sh +44 -1
  87. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -2
  88. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +1 -1
  89. package/scripts/install-claude-plugins.sh +22 -4
  90. package/scripts/two-channel-couplings.json +1 -1
  91. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +96 -12
  92. package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +3 -2
package/README.md CHANGED
@@ -264,6 +264,28 @@ Lisa can watch a queue of ready work and dispatch each item through the lifecycl
264
264
  > **Prompt for your coding agent**
265
265
  > "Which commands let this Lisa scan a work queue, dispatch ready items, repair stuck ones, and report on scheduled-automation health? Show me how I'd point one at my queue and what it expects in configuration."
266
266
 
267
+ ### Running several agents on one machine
268
+
269
+ Test runners size themselves to the machine, not to what else is running on it. Left alone, each run claims roughly one worker per core, so *k* agents working at once claim *k* × cores. That is not merely slow: past a certain point the machine starts terminating processes, and a gate that was killed looks exactly like a gate that failed — so an agent "fixes" a phantom, retries, and adds the load that kills the next run.
270
+
271
+ Lisa caps the pool for you, in three layers:
272
+
273
+ | layer | what it does | when it applies |
274
+ |---|---|---|
275
+ | **Floor** | Half the machine's cores | Always, when this run is the only one |
276
+ | **Divisor** | Floor ÷ concurrent runs, never below 2 workers | Whenever other Lisa runs are live |
277
+ | **Override** | Exactly the number you give it | Whenever you set it |
278
+
279
+ You do not have to configure any of this. A run discovers how many sibling runs are live by looking at the shared scratch namespace, so the divisor applies to a fleet nobody told it about. Two variables are available when you want to steer it:
280
+
281
+ - **`LISA_FLEET_CONCURRENCY`** — state how many runs are coming, rather than letting the run count what has already started. Useful when you are about to launch six agents and want the first one sized for six.
282
+ - **`LISA_VITEST_MAX_WORKERS`** — set the pool size outright. Wins over both other layers, in both directions: raise it above the floor as readily as you lower it.
283
+
284
+ A smaller pool is not automatically safer, which is why the floor is a proportion rather than a small constant and why the divisor stops at two workers. Below that a suite serialises, every file waits behind every other file, and per-test time budgets start expiring even as machine load falls — trading a visible failure for a subtler one.
285
+
286
+ > **Prompt for your coding agent**
287
+ > "Show me how this project's test worker pool is sized — find the resolver, tell me what each of the three layers would give on this machine right now, and how many sibling runs it currently detects."
288
+
267
289
  ## Working across trackers and sources
268
290
 
269
291
  Lisa is deliberately vendor-neutral. The lifecycle runs the same whether your tickets live in one tracker or another and whether your product specs originate in one document tool or another — a thin dispatch layer selects the right integration from configuration, so the workflow you learn transfers.
@@ -1690,7 +1690,50 @@ fi
1690
1690
 
1691
1691
  # 13. Dropping or truncating a database / schema / table (Lisa-only guard —
1692
1692
  # upstream has no SQL protection; keep).
1693
- if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
1693
+ #
1694
+ # The two arms used to be asymmetric, and only the lopsided one produced false
1695
+ # positives (issue #3530). The `drop` arm requires a following keyword —
1696
+ # `drop database|schema|table` — so `drop the ball` never matched it. The
1697
+ # `truncate` arm made that keyword OPTIONAL:
1698
+ #
1699
+ # truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+
1700
+ #
1701
+ # which reduces to "`truncate`, whitespace, any identifier-ish word", so
1702
+ # `truncate the board`, `truncate results` and `truncate output` all matched.
1703
+ # A session was refused for a bug-report TITLE describing a connection that
1704
+ # truncates results — and a title has no escape hatch, because the remedy this
1705
+ # hook prints at the wall (write the text to a file, pass it BY PATH) covers a
1706
+ # body, while the title flag of an issue-filing CLI is necessarily inline. The
1707
+ # only exit left was to reword, which is what trains people to route around a
1708
+ # guard.
1709
+ #
1710
+ # The fix keeps the coverage and drops the English, in three arms.
1711
+ #
1712
+ # ARM 1 — `drop`, byte-identical to before. It was already correct, and a
1713
+ # rewrite that unified the arms could only make it permissive.
1714
+ readonly SQL_DROP='\bdrop[[:space:]]+(database|schema|table)\b'
1715
+ # ARM 2 — `truncate` carrying its own evidence that it is a statement rather
1716
+ # than a verb: the TABLE keyword (what the drop arm demands), a
1717
+ # schema-qualified name, a quoted identifier, a trailing `;`, or one of the
1718
+ # tail keywords that only ever follow a TRUNCATE. English has none of these.
1719
+ readonly SQL_TRUNCATE_TAIL='(cascade|restrict|restart[[:space:]]+identity|continue[[:space:]]+identity)([^[:alnum:]_]|$)'
1720
+ readonly SQL_TRUNCATE_EVIDENCE='\btruncate[[:space:]]+(table[[:space:]]+[[:alnum:]_."`]+|(only[[:space:]]+)?([[:alnum:]_]+\.[[:alnum:]_."`]+|["`][[:alnum:]_. ]+["`]|[[:alnum:]_]+[[:space:]]*(;|'"$SQL_TRUNCATE_TAIL"')))'
1721
+ # ARM 3 — the permissive-dialect `TRUNCATE <name>` with none of that evidence.
1722
+ # Deleting this shape is what a lazy fix does: it passes every prose scenario
1723
+ # while silently removing the coverage a regression fixture already asserts
1724
+ # (`mysql -e 'TRUNCATE sessions'`). So it is recognized by CONTEXT instead —
1725
+ # the same command must also invoke a command-line database client. Prose in a
1726
+ # tracker filing or a commit message invokes none, which is exactly the
1727
+ # difference the text scan could not see on its own.
1728
+ readonly SQL_CLIENT='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?(psql|pgcli|mysql|mysqladmin|mysqlsh|mariadb|mycli|sqlite3|sqlcmd|cqlsh|clickhouse-client|cockroach|mongosh|duckdb|usql)([[:space:]]|$)'
1729
+ readonly SQL_TRUNCATE_BARE='\btruncate[[:space:]]+(only[[:space:]]+)?[[:alnum:]_."`]'
1730
+ # Residual, accepted and stated: a bare `TRUNCATE <name>` with no terminator,
1731
+ # no qualification and no recognized client in the same command goes unmatched
1732
+ # — the same trade the `drop` arm has always made for `drop the ball`. No DROP
1733
+ # shape is weakened, and the guard still fails in the safe direction.
1734
+ if matches "$SQL_DROP" \
1735
+ || matches "$SQL_TRUNCATE_EVIDENCE" \
1736
+ || { matches "$SQL_CLIENT" && matches "$SQL_TRUNCATE_BARE"; }; then
1694
1737
  block "destructive SQL (DROP/TRUNCATE) detected"
1695
1738
  fi
1696
1739
 
@@ -25,6 +25,14 @@ const RELEASE_SUBJECT =
25
25
  /^chore\(release\): \d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)? \[skip ci\]$/;
26
26
  const ZERO_OID = /^0+$/;
27
27
  const MARKER = "[lisa-pr-link]";
28
+ /**
29
+ * The deploy environment whose done role is terminal — the only one that
30
+ * closes a work item. Named once because three separate readers compare
31
+ * against it, and a literal repeated three times is three places to drift.
32
+ */
33
+ const PRODUCTION = "production";
34
+ /** How a base branch that maps to no deploy environment is described. */
35
+ const NOT_A_DEPLOY_BRANCH = "not a deploy branch";
28
36
  /**
29
37
  * The command that establishes the ticket-side backlink.
30
38
  *
@@ -513,8 +521,11 @@ function lifecycleContract(config, provider) {
513
521
  // state is called `Done` — a message that sends someone looking for a state
514
522
  // that is not what they configured and not what Linear shows them.
515
523
  const terminalName = String(terminal ?? "");
524
+ const byEnvironment = doneRolesByEnvironment(roles.done, terminalName);
516
525
  return {
517
526
  claimed: requireString(roles.claimed, `${provider} claimed lifecycle role`),
527
+ done: byEnvironment,
528
+ productionEnvironment: productionEnvironmentOf(byEnvironment, terminalName),
518
529
  ready: requireString(roles.ready, `${provider} ready lifecycle role`),
519
530
  roles: lifecycleRoleSet(roles, done),
520
531
  terminal: terminalName.toLowerCase(),
@@ -522,6 +533,55 @@ function lifecycleContract(config, provider) {
522
533
  };
523
534
  }
524
535
 
536
+ /**
537
+ * The done roles keyed by deploy environment, in configured order.
538
+ *
539
+ * The single-string form (`done: "Done"`) is a project that names one done
540
+ * role for every environment, so it is read as the production one — that is
541
+ * the only environment whose role closes anything.
542
+ * @param {unknown} done The configured `done` role or role map.
543
+ * @param {string} terminalName The resolved terminal role's spelling.
544
+ * @returns {[string, string][]} `[environment, role]` pairs, order preserved.
545
+ */
546
+ function doneRolesByEnvironment(done, terminalName) {
547
+ const entries =
548
+ typeof done === "string"
549
+ ? [[PRODUCTION, done]]
550
+ : done && typeof done === "object"
551
+ ? Object.entries(done)
552
+ : [];
553
+ const mapped = entries
554
+ .map(([environment, role]) => [
555
+ String(environment).trim().toLowerCase(),
556
+ typeof role === "string" ? role.trim() : "",
557
+ ])
558
+ .filter(([environment, role]) => environment !== "" && role !== "");
559
+ // A shape this reader could not decompose must never leave the writer with
560
+ // NO environment to apply, which would refuse every completion in a project
561
+ // whose `done` nests. The resolved terminal is always an answer.
562
+ return mapped.length > 0 ? mapped : [[PRODUCTION, terminalName]];
563
+ }
564
+
565
+ /**
566
+ * Which of the configured environments is the production one.
567
+ *
568
+ * Found by matching the already-resolved TERMINAL ROLE, not by position. The
569
+ * terminal role is the single existing answer to "which role closes an item",
570
+ * so deriving the environment from it means the two can never disagree — and
571
+ * it avoids deriving anything a second, independent way from JSON key order,
572
+ * which is not semantically meaningful and which no schema constrains.
573
+ * @param {[string, string][]} byEnvironment `[environment, role]` pairs.
574
+ * @param {string} terminalName The resolved terminal role's spelling.
575
+ * @returns {string} The production environment's name.
576
+ */
577
+ function productionEnvironmentOf(byEnvironment, terminalName) {
578
+ const folded = terminalName.trim().toLowerCase();
579
+ const matched = byEnvironment.find(
580
+ ([, role]) => role.toLowerCase() === folded
581
+ );
582
+ return matched?.[0] ?? PRODUCTION;
583
+ }
584
+
525
585
  /**
526
586
  * Every lifecycle role a project has configured, in the project's own spelling.
527
587
  *
@@ -670,6 +730,46 @@ function verifyLevel(config) {
670
730
  return value;
671
731
  }
672
732
 
733
+ /**
734
+ * Deploy branches reversed: branch name -> the environment it deploys.
735
+ *
736
+ * `deploy.branches` is written env-first (`production: main`) because that is
737
+ * how a human thinks about it. Every reader here starts from a branch — a
738
+ * merged pull request's base — and needs the environment, so the map is
739
+ * inverted once, here, rather than searched at each use.
740
+ *
741
+ * **The fallback is deliberate and narrow.** A project that configures no
742
+ * deploy branches at all is not saying "nothing ever deploys"; it is saying
743
+ * nothing unusual happens, and its default branch is its production branch.
744
+ * Treating the absent map as "no branch deploys anything" would refuse every
745
+ * completion in every project that never wrote the key — a fix that stops
746
+ * completing anything is not a fix. What the fallback does NOT do is invent an
747
+ * environment for a branch that is simply not in a map the project DID write:
748
+ * that is the stacked-integration case, and it is exactly the one that must
749
+ * not be read as production.
750
+ * @param {object} config The resolved Lisa configuration.
751
+ * @returns {Map<string, string>} Branch name to deploy environment.
752
+ */
753
+ export function deployBranchEnvironments(config) {
754
+ const configured = config?.deploy?.branches;
755
+ const entries =
756
+ configured && typeof configured === "object"
757
+ ? Object.entries(configured)
758
+ : [];
759
+ const mapped = new Map();
760
+ for (const [environment, branch] of entries) {
761
+ const name = typeof branch === "string" ? branch.trim() : "";
762
+ if (name !== "") mapped.set(name, String(environment).trim().toLowerCase());
763
+ }
764
+ if (mapped.size > 0) return mapped;
765
+ const fallback = config?.policy?.repository?.default_branch;
766
+ const branch =
767
+ typeof fallback === "string" && fallback.trim() !== ""
768
+ ? fallback.trim()
769
+ : "main";
770
+ return new Map([[branch, PRODUCTION]]);
771
+ }
772
+
673
773
  function trackerContract(config = readConfig()) {
674
774
  const provider = requireString(config.tracker, "tracker").toLowerCase();
675
775
  const identityRepo = currentRepoIdentity(config);
@@ -686,6 +786,7 @@ function trackerContract(config = readConfig()) {
686
786
  provider,
687
787
  repository,
688
788
  identityRepo,
789
+ deployBranches: deployBranchEnvironments(config),
689
790
  verify: verifyLevel(config),
690
791
  lifecycle: lifecycleContract(config, provider),
691
792
  repositoryIsIdentity:
@@ -3127,9 +3228,193 @@ function githubTimeline(ref) {
3127
3228
  }
3128
3229
 
3129
3230
  /**
3130
- * Move a work item to its configured terminal role and close it.
3231
+ * The branch a merged pull request was merged INTO.
3131
3232
  *
3132
- * The terminal role is RESOLVED from configuration, never assumed. Lisa's own
3233
+ * A second call per pull request, because the timeline cannot answer it: a
3234
+ * `cross-referenced` event carries the referencing item in ISSUE shape, and an
3235
+ * issue has no base branch. Reading the base is the whole point — without it
3236
+ * "a merged pull request references this item" is the only evidence there is,
3237
+ * and that sentence is true of a merge into an integration branch that shipped
3238
+ * nothing.
3239
+ * @param {string} repository `owner/name` the pull request belongs to.
3240
+ * @param {number} number The pull-request number.
3241
+ * @returns {string} The base branch name, empty when it could not be read.
3242
+ */
3243
+ function pullRequestBaseBranch(repository, number) {
3244
+ const result = run(
3245
+ "gh",
3246
+ ["api", `repos/${repository}/pulls/${number}`, "--jq", ".base.ref"],
3247
+ { allowFailure: true }
3248
+ );
3249
+ if (result.status !== 0)
3250
+ throw githubFailure(result, `${repository}#${number}`);
3251
+ return result.stdout.trim();
3252
+ }
3253
+
3254
+ /**
3255
+ * Decide which lifecycle role a set of merged pull requests has earned.
3256
+ *
3257
+ * The defect this exists to end: completion read "is there a merged pull
3258
+ * request referencing this item" and never asked which branch it merged into,
3259
+ * so a stacked pull request landing on an integration branch stamped the
3260
+ * PRODUCTION terminal role and closed the item. Nothing had reached the deploy
3261
+ * branch and nothing had deployed, but every recovery flow — sweep, repair
3262
+ * intake, the rollup reconcilers — reads a closed item carrying the terminal
3263
+ * role as finished, so the corruption is in the direction that stops recovery
3264
+ * from firing.
3265
+ *
3266
+ * **Production is recognised, never ranked.** An earlier draft of this
3267
+ * function ordered environments by their POSITION in the configured `done`
3268
+ * map, which makes correctness depend on JSON key order — not semantically
3269
+ * meaningful, constrained by no schema, and able to fail in the direction of
3270
+ * writing the WRONG TERMINAL ROLE rather than merely refusing, which is the
3271
+ * original defect with extra steps. So the only distinction drawn here is
3272
+ * production versus not: a merge into the production environment is terminal,
3273
+ * a single non-production environment records its own role, and several
3274
+ * different non-production environments are reported rather than ranked. That
3275
+ * is enough, because the role this ticket is about is the terminal one.
3276
+ *
3277
+ * Pure and exported so the decision can be asserted without a network.
3278
+ * @param {{base: string, number: number}[]} pullRequests Merged pull requests.
3279
+ * @param {Map<string, string>} branchEnvironments Branch to deploy environment.
3280
+ * @param {{done: [string, string][], productionEnvironment: string}} lifecycle
3281
+ * The resolved lifecycle contract.
3282
+ * @returns {{ambiguous: boolean, environment: string|null, evidence: {base: string, environment: string|null, number: number}[], role: string|null, terminal: boolean}}
3283
+ * The role earned, or a null role when nothing was earned or nothing ranks.
3284
+ */
3285
+ export function mergedBaseDecision(
3286
+ pullRequests,
3287
+ branchEnvironments,
3288
+ lifecycle
3289
+ ) {
3290
+ const roles = new Map(lifecycle?.done ?? []);
3291
+ const production = lifecycle?.productionEnvironment;
3292
+ const evidence = (pullRequests ?? []).map(request => {
3293
+ const base = String(request?.base ?? "");
3294
+ return {
3295
+ base,
3296
+ environment: branchEnvironments?.get(base) ?? null,
3297
+ number: request?.number,
3298
+ };
3299
+ });
3300
+ const reached = [
3301
+ ...new Set(
3302
+ evidence
3303
+ .map(entry => entry.environment)
3304
+ .filter(environment => roles.has(environment))
3305
+ ),
3306
+ ];
3307
+ if (reached.includes(production)) {
3308
+ return {
3309
+ ambiguous: false,
3310
+ environment: production,
3311
+ evidence,
3312
+ role: roles.get(production),
3313
+ terminal: true,
3314
+ };
3315
+ }
3316
+ // Exactly one non-production environment is unambiguous. Two different ones
3317
+ // would need an ordering the configuration does not state, so they are
3318
+ // reported instead of guessed at.
3319
+ if (reached.length !== 1) {
3320
+ return {
3321
+ ambiguous: reached.length > 1,
3322
+ environment: null,
3323
+ evidence,
3324
+ role: null,
3325
+ terminal: false,
3326
+ };
3327
+ }
3328
+ return {
3329
+ ambiguous: false,
3330
+ environment: reached[0],
3331
+ evidence,
3332
+ role: roles.get(reached[0]),
3333
+ terminal: false,
3334
+ };
3335
+ }
3336
+
3337
+ /**
3338
+ * Name every merged pull request and the branch it landed on.
3339
+ *
3340
+ * An operator reading a run that completed nothing has to be able to tell WHY
3341
+ * without opening the code, and "which branch did it actually merge into" is
3342
+ * the whole answer.
3343
+ * @param {{base: string, environment: string|null, number: number}[]} evidence
3344
+ * What each merged pull request was observed to target.
3345
+ * @returns {string} A readable one-line summary.
3346
+ */
3347
+ function describeMergeBases(evidence) {
3348
+ return evidence
3349
+ .map(
3350
+ entry =>
3351
+ `#${entry.number} -> ${entry.base || "(unknown base)"} [${entry.environment ?? NOT_A_DEPLOY_BRANCH}]`
3352
+ )
3353
+ .join(", ");
3354
+ }
3355
+
3356
+ /**
3357
+ * Name the branches this project does deploy, and which one it took to be
3358
+ * production.
3359
+ *
3360
+ * Naming the production branch explicitly is what makes a wrong ASSUMPTION
3361
+ * visible. A project that never wrote `deploy.branches` has its default branch
3362
+ * read as production; if that is not its real production branch, every
3363
+ * completion refuses and the operator has to be able to see the assumption in
3364
+ * the refusal rather than infer it from a list.
3365
+ * @param {Map<string, string>} branchEnvironments Branch to deploy environment.
3366
+ * @param {string} production The production environment's name.
3367
+ * @returns {string} A readable list, with the production branch called out.
3368
+ */
3369
+ function describeDeployBranches(branchEnvironments, production) {
3370
+ const listed =
3371
+ [...branchEnvironments]
3372
+ .map(([branch, environment]) => `${branch} [${environment}]`)
3373
+ .join(", ") || "(none configured)";
3374
+ const [productionBranch] =
3375
+ [...branchEnvironments].find(
3376
+ ([, environment]) => environment === production
3377
+ ) ?? [];
3378
+ return productionBranch === undefined
3379
+ ? `${listed}; no branch maps to "${production}"`
3380
+ : `${listed}; production is taken to be "${productionBranch}"`;
3381
+ }
3382
+
3383
+ /**
3384
+ * Record a deploy role that is NOT the production terminal.
3385
+ *
3386
+ * Adds the environment's role and stops. It deliberately does not close the
3387
+ * item and does not retire the claimed role: the work is genuinely still in
3388
+ * flight until it reaches the production branch, and native closure fires only
3389
+ * at the production terminal.
3390
+ * @param {string} ref Canonical work-item reference.
3391
+ * @param {{labels: string[]}} before The pre-write tracker state.
3392
+ * @param {string} role The environment's configured done role.
3393
+ * @returns {void}
3394
+ */
3395
+ function recordDeployRole(ref, before, role) {
3396
+ if (carriesLabel(before.labels, role)) return;
3397
+ const [repository, number] = ref.split("#");
3398
+ const edited = run(
3399
+ "gh",
3400
+ [
3401
+ "issue",
3402
+ "edit",
3403
+ String(number),
3404
+ "--repo",
3405
+ repository,
3406
+ "--add-label",
3407
+ role,
3408
+ ],
3409
+ { allowFailure: true }
3410
+ );
3411
+ if (edited.status !== 0) throw githubFailure(edited, ref);
3412
+ }
3413
+
3414
+ /**
3415
+ * Move a work item to the lifecycle role its merges have actually earned.
3416
+ *
3417
+ * The role is RESOLVED from configuration, never assumed. Lisa's own
3133
3418
  * repository maps `production` to `status:done`, but the map is environment
3134
3419
  * aware — a project whose target is `dev` has a different terminal role, and a
3135
3420
  * hardcoded label would silently apply the wrong one there while looking
@@ -3139,9 +3424,18 @@ function githubTimeline(ref) {
3139
3424
  * is pointed at is a way to make unfinished work disappear, which is the same
3140
3425
  * defect class as a gate that passes because it found nothing. Evidence is a
3141
3426
  * merged pull request in this repository.
3427
+ *
3428
+ * **And a merged pull request is not evidence on its own.** The base branch it
3429
+ * merged into decides which role it earned: only a merge into the production
3430
+ * deploy branch is terminal. A merge into an integration branch records the
3431
+ * environment's role at most, and a merge into a branch this project does not
3432
+ * deploy at all records nothing and closes nothing — reported, never silent,
3433
+ * because a completion that quietly did not happen is the same failure as one
3434
+ * that wrongly did.
3142
3435
  * @param {string} ref Canonical work-item reference.
3143
3436
  * @param {object} contract The resolved tracker contract.
3144
- * @returns {{merged: number[], terminal: string}} What was applied, and why.
3437
+ * @returns {{applied: boolean, merged: number[], report: string, terminal: string|null}}
3438
+ * What was applied, and why.
3145
3439
  */
3146
3440
  function completeGithubWorkItem(ref, contract) {
3147
3441
  const [repository] = ref.split("#");
@@ -3154,11 +3448,51 @@ function completeGithubWorkItem(ref, contract) {
3154
3448
  `from a real one afterwards. If the work shipped some other way, say so on the item and close it deliberately.`
3155
3449
  );
3156
3450
  }
3451
+ const decision = mergedBaseDecision(
3452
+ merged.map(number => ({
3453
+ base: pullRequestBaseBranch(repository, number),
3454
+ number,
3455
+ })),
3456
+ contract.deployBranches,
3457
+ contract.lifecycle
3458
+ );
3459
+ const evidence = merged.map(number => `#${number}`).join(", ");
3460
+ const bases = describeMergeBases(decision.evidence);
3461
+ if (decision.role === null) {
3462
+ const why = decision.ambiguous
3463
+ ? `Several different non-production deploy environments were reached, and the configuration states\n` +
3464
+ `no ordering between them, so this writer reports them rather than guessing which is furthest.`
3465
+ : `No merged pull request reached a branch this project deploys, so no lifecycle role was\n` +
3466
+ `applied and ${ref} stays open. It completes when the change reaches a deploy branch.`;
3467
+ return {
3468
+ applied: false,
3469
+ bases,
3470
+ merged,
3471
+ report:
3472
+ `work-item NOT completed: ${ref} (merged: ${evidence}) bases: ${bases}\n` +
3473
+ `${why}\n` +
3474
+ `Deploy branches: ${describeDeployBranches(contract.deployBranches, contract.lifecycle.productionEnvironment)}.\n` +
3475
+ `If that production branch is wrong, "deploy.branches" in .lisa.config.json is what to fix.`,
3476
+ terminal: null,
3477
+ };
3478
+ }
3157
3479
  const before = githubLifecycleState(
3158
3480
  ref,
3159
3481
  `GitHub issue ${ref} completion read`
3160
3482
  );
3161
3483
  assertNotAbandoned(ref, before);
3484
+ if (!decision.terminal) {
3485
+ recordDeployRole(ref, before, decision.role);
3486
+ return {
3487
+ applied: true,
3488
+ bases,
3489
+ merged,
3490
+ report:
3491
+ `work-item advanced: ${ref} -> ${decision.role} (merged: ${evidence}) bases: ${bases}\n` +
3492
+ `${decision.environment} is not the production environment, so ${ref} stays open.`,
3493
+ terminal: decision.role,
3494
+ };
3495
+ }
3162
3496
  reconcileGithubLifecycle(ref, before, contract);
3163
3497
  // A SECOND read, deliberately. The edit and the close each reported their own
3164
3498
  // success, and a writer that trusts those is asserting about tracker state it
@@ -3168,7 +3502,13 @@ function completeGithubWorkItem(ref, contract) {
3168
3502
  `GitHub issue ${ref} completion readback`
3169
3503
  );
3170
3504
  assertCompletionReadback(ref, after, contract);
3171
- return { merged, terminal: contract.lifecycle.terminalName };
3505
+ return {
3506
+ applied: true,
3507
+ bases,
3508
+ merged,
3509
+ report: `work-item completed: ${ref} -> ${contract.lifecycle.terminalName} (merged: ${evidence}) bases: ${bases}`,
3510
+ terminal: contract.lifecycle.terminalName,
3511
+ };
3172
3512
  }
3173
3513
 
3174
3514
  /**
@@ -3582,11 +3922,15 @@ function complete(args) {
3582
3922
  }
3583
3923
  const ref = canonicalizeRef(supplied ?? bound, contract);
3584
3924
  const prUrl = pullRequestUrlOption(args);
3585
- const { merged, terminal } = completeWorkItem(ref, contract, prUrl);
3925
+ const outcome = completeWorkItem(ref, contract, prUrl);
3926
+ // The writer composes its own sentence when the base branch shaped the
3927
+ // outcome, because only the writer knows which bases it read. The Linear
3928
+ // path has no such decision to report, so it keeps the original line.
3586
3929
  console.log(
3587
- `work-item completed: ${ref} -> ${terminal} (merged: ${merged
3588
- .map(number => `#${number}`)
3589
- .join(", ")})`
3930
+ outcome.report ??
3931
+ `work-item completed: ${ref} -> ${outcome.terminal} (merged: ${outcome.merged
3932
+ .map(number => `#${number}`)
3933
+ .join(", ")})`
3590
3934
  );
3591
3935
  }
3592
3936
 
@@ -3645,8 +3989,15 @@ function sweep(args) {
3645
3989
  console.log(`DRIFT ${ref} merged: ${evidence} ${issue.title}`);
3646
3990
  continue;
3647
3991
  }
3648
- const { terminal } = completeWorkItem(ref, contract);
3649
- console.log(`completed ${ref} -> ${terminal} (merged: ${evidence})`);
3992
+ const outcome = completeWorkItem(ref, contract);
3993
+ // A sweep must not abort on the first item whose merges have not reached a
3994
+ // deploy branch. It reports that item and carries on, which is what makes
3995
+ // the backstop safe to run over a whole queue.
3996
+ console.log(
3997
+ outcome.applied
3998
+ ? `completed ${ref} -> ${outcome.terminal} (merged: ${evidence}) bases: ${outcome.bases}`
3999
+ : outcome.report
4000
+ );
3650
4001
  }
3651
4002
  if (drifted === 0) {
3652
4003
  console.log(
@@ -93,11 +93,18 @@ export declare function worktreeExclusions(): readonly string[];
93
93
  */
94
94
  export declare const MAX_WORKERS_OVERRIDE_VAR = "LISA_VITEST_MAX_WORKERS";
95
95
  /**
96
- * Environment variable naming how many concurrent runs share this machine.
96
+ * Environment variable stating how many concurrent runs share this machine.
97
97
  *
98
- * Set by whatever launches a fleet of agents. Absent for an ordinary developer,
99
- * which is the point: the divisor applies only when something states that this
100
- * run is one of several, so the single-run case is never throttled by it.
98
+ * Optional, and it is a statement of INTENT rather than an observation: "six
99
+ * runs are coming", which is knowledge a count of what is live right now cannot
100
+ * have. So it outranks {@link discoverFleetConcurrency} when present.
101
+ *
102
+ * It is no longer the only way the divisor is reached, and that correction is
103
+ * the whole of #3665. Shipped as the sole mechanism, it was set by nothing —
104
+ * one occurrence in the tree, its own declaration — so the divisor was inert
105
+ * for every consumer and the cap arrived as floor-only. Discovery is the layer
106
+ * that reaches a run nobody configured; this variable is now the way to
107
+ * override what discovery sees.
101
108
  */
102
109
  export declare const FLEET_CONCURRENCY_VAR = "LISA_FLEET_CONCURRENCY";
103
110
  /**
@@ -122,6 +129,56 @@ export declare const DEFAULT_MAX_WORKERS = "50%";
122
129
  * a visible kill for an invisible timeout has not helped anyone.
123
130
  */
124
131
  export declare const MIN_FLEET_WORKERS = 2;
132
+ /**
133
+ * How many Lisa test runs are live on this machine right now, including this one.
134
+ *
135
+ * ## Why this is discovered rather than declared
136
+ *
137
+ * The divisor shipped as an explicit signal, and for a release nothing set it:
138
+ * `LISA_FLEET_CONCURRENCY` occurred exactly once in the tree, at its own
139
+ * declaration. So the cap reached every consumer as floor-only, and the layer
140
+ * that addresses multi-agent contention was inert by default
141
+ * (CodySwannGT/lisa#3665).
142
+ *
143
+ * The obvious repair — pick a component and have it export the variable — fails
144
+ * its own test. **Nothing knows how many runs it is about to start.** Agents are
145
+ * launched independently, each in its own worktree, by whoever happens to be
146
+ * driving; there is no scheduler holding that number. A setter would have to
147
+ * guess, and a guessed fleet size that goes stale the moment the fleet resizes
148
+ * is a worse contract than no setter at all.
149
+ *
150
+ * What every run *can* do is look. Each supervised run already registers a
151
+ * `run-<pid>-<epoch>-<suffix>` root inside one namespace under `os.tmpdir()`,
152
+ * which on this platform is per-user and therefore shared by every checkout on
153
+ * the machine. Counting the roots whose owning process is still alive answers
154
+ * the question at the only moment it matters — when this run is about to size
155
+ * its pool — without anybody having to know the answer in advance.
156
+ *
157
+ * ## Fail open, never closed
158
+ *
159
+ * This runs inside a Vitest config factory, so a throw here does not degrade a
160
+ * test run, it prevents one. Every failure path returns 1 (no fleet detected),
161
+ * which yields the floor — the behaviour that shipped. An unreadable namespace,
162
+ * a permission error, a platform without `process.kill`, an absent directory on
163
+ * the very first run: all of them mean "no evidence of siblings", never "assume
164
+ * the worst and throttle".
165
+ *
166
+ * Our own pid is excluded rather than subtracted, because the config may load
167
+ * before or after this run registers its own root, and both orders must give
168
+ * the same count. The result adds one back for this run.
169
+ * @param deps - Injectable seams so tests state a fleet instead of spawning one.
170
+ * @param deps.namespaceDir - Directory holding run roots.
171
+ * @param deps.readDir - Directory lister.
172
+ * @param deps.isAlive - Process-liveness probe.
173
+ * @param deps.self - This process's id.
174
+ * @returns Live run count including this one; 1 when nothing else is detected.
175
+ */
176
+ export declare function discoverFleetConcurrency(deps?: {
177
+ namespaceDir?: () => string;
178
+ readDir?: (dir: string) => readonly string[];
179
+ isAlive?: (pid: number) => boolean;
180
+ self?: number;
181
+ }): number;
125
182
  /**
126
183
  * Resolves the Vitest worker-pool cap for this run.
127
184
  *
@@ -142,14 +199,21 @@ export declare const MIN_FLEET_WORKERS = 2;
142
199
  * recorded, not hypothesised — see {@link MIN_FLEET_WORKERS}. So the resolution
143
200
  * has three layers, and only the middle one knows about the fleet:
144
201
  *
145
- * 1. **Floor.** {@link DEFAULT_MAX_WORKERS}, always. This is the only layer that
146
- * reaches a run nobody configured, which is most of them.
147
- * 2. **Divisor.** When {@link FLEET_CONCURRENCY_VAR} states that this run is one
148
- * of *k*, the floor is divided by *k* so the fleet's total pool is bounded by
149
- * the machine rather than multiplied by its own size — never below
150
- * {@link MIN_FLEET_WORKERS}.
202
+ * 1. **Floor.** {@link DEFAULT_MAX_WORKERS}, when this run is the only one.
203
+ * 2. **Divisor.** The floor divided by the number of concurrent runs, so the
204
+ * fleet's total pool is bounded by the machine rather than multiplied by its
205
+ * own size — never below {@link MIN_FLEET_WORKERS}. That number comes from
206
+ * {@link FLEET_CONCURRENCY_VAR} when something states it, and otherwise from
207
+ * {@link discoverFleetConcurrency}, which counts live sibling runs.
208
+ * Discovery is what makes this layer reach a run nobody configured; as an
209
+ * opt-in signal it reached nothing for a release (#3665).
151
210
  * 3. **Override.** {@link MAX_WORKERS_OVERRIDE_VAR} replaces both, upward or
152
211
  * downward.
212
+ *
213
+ * A stated signal outranks discovery rather than being reconciled with it. An
214
+ * operator who says "we are six" is describing an intent — six runs are coming,
215
+ * even if only two have started — and a count of what happens to be live right
216
+ * now must not quietly contradict it.
153
217
  * ## Why the environment is imported rather than reached for
154
218
  *
155
219
  * The default comes from `node:process`'s `env` binding, not from the ambient
@@ -162,9 +226,10 @@ export declare const MIN_FLEET_WORKERS = 2;
162
226
  * parameter injectable so tests state an environment rather than mutating one.
163
227
  * @param environment - Environment to read; defaults to this process's.
164
228
  * @param cores - Logical cores available; defaults to this machine's count.
229
+ * @param discover - Fleet-size discovery; defaults to counting live run roots.
165
230
  * @returns A value for Vitest's `maxWorkers` — a worker count, or a percentage.
166
231
  */
167
- export declare function resolveMaxWorkers(environment?: NodeJS.ProcessEnv, cores?: number): number | string;
232
+ export declare function resolveMaxWorkers(environment?: NodeJS.ProcessEnv, cores?: number, discover?: () => number): number | string;
168
233
  /**
169
234
  * Setup files that bound and reclaim the suite's temporary directories.
170
235
  *
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/configs/vitest/base.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAIpD,8DAA8D;AAC9D,KAAK,UAAU,GAAG,cAAc,CAAC;AAEjC;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE;QAChB,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF;;;;;;;;OAQG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,kBAO/B,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAatD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAGlD,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAItD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,4BAA4B,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,2BAA2B,CAAC;AAE9D;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,QAAQ,CAAC;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAenC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,GAAE,MAAM,CAAC,UAAgB,EACpC,KAAK,GAAE,MAA+B,GACrC,MAAM,GAAG,MAAM,CASjB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAKrD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,SAAS,MAAM,EAAE,CAEtD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACxB,YAAY,kBAAkB,KAC7B,gBAaD,CAAC;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe,GAC1B,UAAU,kBAAkB,EAC5B,WAAW,kBAAkB,KAC5B,kBAiBF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,GAAI,GAAG,SAAS,UAAU,EAAE,KAAG,UA4C7D,CAAC"}
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/configs/vitest/base.ts"],"names":[],"mappings":"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"}