@codyswann/lisa 4.22.11 → 4.22.13

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 (181) hide show
  1. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +4 -2
  2. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +62 -11
  3. package/all/copy-overwrite/scripts/lisa-environment-prepare.mjs +10 -1
  4. package/all/copy-overwrite/scripts/lisa-floor-collisions.mjs +1 -0
  5. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +43 -2
  6. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +19 -5
  7. package/all/copy-overwrite/scripts/lisa-reconcile-policy.mjs +13 -1
  8. package/all/copy-overwrite/scripts/lisa-schema-validate.mjs +2 -2
  9. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  10. package/dist/core/lisa-owned-hash-ledger.js +24 -0
  11. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  12. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +5 -0
  13. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts.map +1 -1
  14. package/dist/core/nightly-e2e-guard-behavior-certificate.js +8 -2
  15. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  16. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  17. package/dist/core/upstream-evidence-manifest.js +34 -32
  18. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  19. package/dist/opencode/plugin-templates/block-no-verify.sh +43 -2
  20. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +4 -3
  21. package/dist/opencode/plugin-templates/lisa-block-no-verify.ts +11 -2
  22. package/dist/opencode/plugin-templates/parity-safety-net.sh +19 -5
  23. package/dist/strategies/copy-contents.d.ts +1 -1
  24. package/dist/strategies/copy-contents.d.ts.map +1 -1
  25. package/dist/strategies/copy-contents.js +6 -2
  26. package/dist/strategies/copy-contents.js.map +1 -1
  27. package/expo/create-only/.github/workflows/nightly-e2e-report.yml +1 -0
  28. package/package.json +3 -3
  29. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  31. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +7 -8
  32. package/plugins/lisa/.codex-plugin/skills/lisa-github-prd-intake/SKILL.md +10 -11
  33. package/plugins/lisa/.codex-plugin/skills/lisa-github-sync/SKILL.md +1 -1
  34. package/plugins/lisa/.codex-plugin/skills/lisa-github-write-prd/SKILL.md +14 -12
  35. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +7 -8
  36. package/plugins/lisa/.codex-plugin/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  37. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +9 -10
  38. package/plugins/lisa/.codex-plugin/skills/lisa-linear-evidence/SKILL.md +1 -1
  39. package/plugins/lisa/.codex-plugin/skills/lisa-linear-sync/SKILL.md +1 -1
  40. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +2 -1
  41. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  42. package/plugins/lisa/.codex-plugin/skills/lisa-tracker-sync/SKILL.md +2 -2
  43. package/plugins/lisa/hooks/block-no-verify.sh +43 -2
  44. package/plugins/lisa/hooks/parity-safety-net.sh +19 -5
  45. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +10 -1
  46. package/plugins/lisa/rules/reference/config-resolution.md +0 -4
  47. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +1 -1
  48. package/plugins/lisa/scripts/resolve-lifecycle-role.mjs +15 -3
  49. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +7 -8
  50. package/plugins/lisa/skills/lisa-github-prd-intake/SKILL.md +10 -11
  51. package/plugins/lisa/skills/lisa-github-sync/SKILL.md +1 -1
  52. package/plugins/lisa/skills/lisa-github-write-prd/SKILL.md +14 -12
  53. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +7 -8
  54. package/plugins/lisa/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  55. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +9 -10
  56. package/plugins/lisa/skills/lisa-linear-evidence/SKILL.md +1 -1
  57. package/plugins/lisa/skills/lisa-linear-sync/SKILL.md +1 -1
  58. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +2 -1
  59. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  60. package/plugins/lisa/skills/lisa-tracker-sync/SKILL.md +2 -2
  61. package/plugins/lisa-agy/hooks/parity-safety-net.sh +19 -5
  62. package/plugins/lisa-agy/plugin.json +1 -1
  63. package/plugins/lisa-agy/scripts/resolve-lifecycle-role.mjs +15 -3
  64. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +7 -8
  65. package/plugins/lisa-agy/skills/lisa-github-prd-intake/SKILL.md +10 -11
  66. package/plugins/lisa-agy/skills/lisa-github-sync/SKILL.md +1 -1
  67. package/plugins/lisa-agy/skills/lisa-github-write-prd/SKILL.md +14 -12
  68. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +7 -8
  69. package/plugins/lisa-agy/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  70. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +9 -10
  71. package/plugins/lisa-agy/skills/lisa-linear-evidence/SKILL.md +1 -1
  72. package/plugins/lisa-agy/skills/lisa-linear-sync/SKILL.md +1 -1
  73. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +2 -1
  74. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  75. package/plugins/lisa-agy/skills/lisa-tracker-sync/SKILL.md +2 -2
  76. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  79. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-copilot/hooks/block-no-verify.sh +43 -2
  83. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +19 -5
  84. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +10 -1
  85. package/plugins/lisa-copilot/rules/reference/config-resolution.md +0 -4
  86. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +1 -1
  87. package/plugins/lisa-copilot/scripts/resolve-lifecycle-role.mjs +15 -3
  88. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +7 -8
  89. package/plugins/lisa-copilot/skills/lisa-github-prd-intake/SKILL.md +10 -11
  90. package/plugins/lisa-copilot/skills/lisa-github-sync/SKILL.md +1 -1
  91. package/plugins/lisa-copilot/skills/lisa-github-write-prd/SKILL.md +14 -12
  92. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +7 -8
  93. package/plugins/lisa-copilot/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  94. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +9 -10
  95. package/plugins/lisa-copilot/skills/lisa-linear-evidence/SKILL.md +1 -1
  96. package/plugins/lisa-copilot/skills/lisa-linear-sync/SKILL.md +1 -1
  97. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +2 -1
  98. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  99. package/plugins/lisa-copilot/skills/lisa-tracker-sync/SKILL.md +2 -2
  100. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-cursor/hooks/block-no-verify.sh +43 -2
  102. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +19 -5
  103. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +10 -1
  104. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +0 -4
  105. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +1 -1
  106. package/plugins/lisa-cursor/scripts/resolve-lifecycle-role.mjs +15 -3
  107. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +7 -8
  108. package/plugins/lisa-cursor/skills/lisa-github-prd-intake/SKILL.md +10 -11
  109. package/plugins/lisa-cursor/skills/lisa-github-sync/SKILL.md +1 -1
  110. package/plugins/lisa-cursor/skills/lisa-github-write-prd/SKILL.md +14 -12
  111. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +7 -8
  112. package/plugins/lisa-cursor/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  113. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +9 -10
  114. package/plugins/lisa-cursor/skills/lisa-linear-evidence/SKILL.md +1 -1
  115. package/plugins/lisa-cursor/skills/lisa-linear-sync/SKILL.md +1 -1
  116. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +2 -1
  117. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  118. package/plugins/lisa-cursor/skills/lisa-tracker-sync/SKILL.md +2 -2
  119. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  120. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  121. package/plugins/lisa-expo-agy/plugin.json +1 -1
  122. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  126. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  127. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  130. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  131. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  132. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  134. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  136. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  137. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  139. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  141. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  142. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  143. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  144. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  146. package/plugins/lisa-rails-agy/plugin.json +1 -1
  147. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  148. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  149. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  150. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  151. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  152. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  154. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  155. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  156. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  157. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  158. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  159. package/plugins/src/base/hooks/block-no-verify.sh +43 -2
  160. package/plugins/src/base/hooks/parity-safety-net.sh +19 -5
  161. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +10 -1
  162. package/plugins/src/base/rules/reference/config-resolution.md +0 -4
  163. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +1 -1
  164. package/plugins/src/base/scripts/resolve-lifecycle-role.mjs +15 -3
  165. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +7 -8
  166. package/plugins/src/base/skills/lisa-github-prd-intake/SKILL.md +10 -11
  167. package/plugins/src/base/skills/lisa-github-sync/SKILL.md +1 -1
  168. package/plugins/src/base/skills/lisa-github-write-prd/SKILL.md +14 -12
  169. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +7 -8
  170. package/plugins/src/base/skills/lisa-jira-journey/scripts/parse-plan.py +54 -12
  171. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +9 -10
  172. package/plugins/src/base/skills/lisa-linear-evidence/SKILL.md +1 -1
  173. package/plugins/src/base/skills/lisa-linear-sync/SKILL.md +1 -1
  174. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +2 -1
  175. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/setup-remote-env.mjs +4 -3
  176. package/plugins/src/base/skills/lisa-tracker-sync/SKILL.md +2 -2
  177. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +10 -1
  178. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +10 -13
  179. package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +34 -3
  180. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +17 -1
  181. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +10 -1
@@ -33,6 +33,7 @@ import sys
33
33
  import urllib.request
34
34
  from base64 import b64encode
35
35
  from pathlib import Path
36
+ from urllib.parse import urlsplit
36
37
 
37
38
 
38
39
  # Intentionally matches the exact local-claim prefixes only. EVIDENCE-REF is a
@@ -66,13 +67,50 @@ def jira_config_candidates():
66
67
  return candidates
67
68
 
68
69
 
70
+ def read_jira_config(config_path):
71
+ """Read the server and login fields from one jira-cli config."""
72
+ server = ""
73
+ login = ""
74
+ with open(config_path) as config:
75
+ for line in config:
76
+ if line.startswith("server:"):
77
+ server = line.split(":", 1)[1].strip()
78
+ elif line.startswith("login:"):
79
+ login = line.split(":", 1)[1].strip()
80
+ return server, login
81
+
82
+
83
+ def server_origin(server):
84
+ """Return the normalized HTTPS origin used as the credential trust key."""
85
+ try:
86
+ parsed = urlsplit(server)
87
+ port = parsed.port
88
+ except ValueError:
89
+ return ""
90
+ if parsed.scheme.lower() != "https" or not parsed.hostname:
91
+ return ""
92
+ authority = parsed.hostname.lower()
93
+ if port and port != 443:
94
+ authority = f"{authority}:{port}"
95
+ return f"https://{authority}"
96
+
97
+
69
98
  def resolve_jira_config_path():
70
- """Resolve the first existing project config, then the home fallback."""
99
+ """Resolve a project config only when its host matches a user trust root."""
71
100
  candidates = jira_config_candidates()
72
- for candidate in candidates:
73
- if candidate.exists():
101
+ home_config = candidates[-1]
102
+ trusted_server = os.environ.get("JIRA_SERVER", "").strip()
103
+ if not trusted_server and home_config.exists():
104
+ trusted_server, _ = read_jira_config(home_config)
105
+ trusted_origin = server_origin(trusted_server)
106
+
107
+ for candidate in candidates[:-1]:
108
+ if not candidate.exists():
109
+ continue
110
+ candidate_server, _ = read_jira_config(candidate)
111
+ if trusted_origin and server_origin(candidate_server) == trusted_origin:
74
112
  return candidate
75
- return candidates[-1]
113
+ return home_config
76
114
 
77
115
 
78
116
  def get_jira_config():
@@ -82,14 +120,18 @@ def get_jira_config():
82
120
  print(f"ERROR: jira-cli config not found at {config_path}", file=sys.stderr)
83
121
  sys.exit(1)
84
122
 
85
- server = ""
86
- login = ""
87
- with open(config_path) as f:
88
- for line in f:
89
- if line.startswith("server:"):
90
- server = line.split(":", 1)[1].strip()
91
- elif line.startswith("login:"):
92
- login = line.split(":", 1)[1].strip()
123
+ server, login = read_jira_config(config_path)
124
+
125
+ trusted_server = os.environ.get("JIRA_SERVER", "").strip()
126
+ trusted_origin = server_origin(trusted_server)
127
+ if trusted_origin:
128
+ if server_origin(server) != trusted_origin:
129
+ print("ERROR: jira-cli config server does not match trusted JIRA_SERVER", file=sys.stderr)
130
+ sys.exit(1)
131
+ server = trusted_server.rstrip("/")
132
+ if not server_origin(server):
133
+ print("ERROR: JIRA server must be an HTTPS URL from a trusted config", file=sys.stderr)
134
+ sys.exit(1)
93
135
 
94
136
  token = os.environ.get("JIRA_API_TOKEN", "")
95
137
  if not token:
@@ -21,20 +21,19 @@ This skill is the destination of the `lisa-tracker-build-intake` shim when `trac
21
21
  Build-queue **workflow state** names are read from `.lisa.config.json` `linear.workflow.*`, falling back to defaults documented in the `config-resolution` rule. Bash pattern:
22
22
 
23
23
  ```bash
24
- # Read role with default fallback. Local overrides global per-key.
25
24
  read_role() {
26
25
  # Single resolver — see config-resolution "The single resolver".
27
26
  # Exit 0 + empty output means an OPTIONAL role is unset: skip that transition,
28
- # never substitute the second argument. A default on an optional role is what
29
- # made "unset" indistinguishable from "not customized".
30
- node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
31
- --role "$1" --vendor linear --intent "${3:-read}" 2>/dev/null
27
+ # never substitute a default. Any non-zero status is a resolver failure and
28
+ # remains visible to the caller.
29
+ node "${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-plugins/lisa}}/scripts/resolve-lifecycle-role.mjs" \
30
+ --role "$1" --vendor linear --intent "${2:-read}"
32
31
  }
33
32
 
34
- READY=$(read_role ready "Ready")
35
- CLAIMED=$(read_role claimed "In Progress")
36
- REVIEW=$(read_role review "In Review")
37
- BLOCKED=$(read_role blocked "Blocked")
33
+ READY=$(read_role ready read) || exit $?
34
+ CLAIMED=$(read_role claimed write) || exit $?
35
+ REVIEW=$(read_role review write) || exit $?
36
+ BLOCKED=$(read_role blocked read) || exit $?
38
37
  ```
39
38
 
40
39
  For env-keyed `done`, resolve the env first, then look up `done[<env>]`:
@@ -69,7 +68,7 @@ else
69
68
  fi
70
69
  ```
71
70
 
72
- In prose below, the role names refer to the resolved **states**: e.g. "the `ready` state" means whatever `linear.workflow.ready` resolves to (default: `Ready`).
71
+ In prose below, the role names refer to the configured **states**: e.g. "the `ready` state" means whatever `linear.workflow.ready` resolves to. Required roles must be configured; optional roles carry no default.
73
72
 
74
73
  ## Why native states, not labels
75
74
 
@@ -109,6 +109,6 @@ Return:
109
109
  ## Rules
110
110
 
111
111
  - Never modify the Issue description as part of evidence posting — comments only. Description edits go through `lisa-linear-write-issue`.
112
- - Never skip the label transition. The build queue is keyed off the configured `linear.workflow.*` labels; an item that ships without transitioning is invisible to monitoring.
112
+ - Never skip the transition when `review` is configured. An empty `$REVIEW` is the one sanctioned skip (Phase 5): post the evidence comment, leave the Issue in `$CLAIMED`, and report the skip. Whenever `$REVIEW` is non-empty, the configured native state transition is mandatory.
113
113
  - If `lisa-linear-access operation: save-comment` fails, retry once. If it fails again, surface the error — don't pretend the comment was posted.
114
114
  - Do not delete prior comments. The history is the audit trail.
@@ -152,7 +152,7 @@ When the caller passes `--rollup`, this skill **derives a parent/container's wor
152
152
 
153
153
  ## Rules
154
154
 
155
- - Never transition a leaf Issue's workflow `state` from this skill at all. `--rollup` (parent derivation, per the `leaf-only-lifecycle` rule) is the only write path that remains; every other milestone SUGGESTS a transition in its comment and nothing more. The state is the lifecycle lane, so a write here would move the item in the build queue.
155
+ - Never transition a leaf Issue's workflow `state` from this skill except for the narrowly defined Phase 4b backstop that reopens a leaf auto-closed at an intermediate environment. `--rollup` remains the only other write path (parent derivation, per the `leaf-only-lifecycle` rule); every normal milestone SUGGESTS a transition in its comment and nothing more. The state is the lifecycle lane, so an unscoped write here would move the item in the build queue.
156
156
  - Never resolve a role by searching the team's states. Only a name the project configured may be written; a `type`- or position-derived match may inform a read and must never supply a write target (`config-resolution`, R2).
157
157
  - Rollup derives a *parent's* workflow state from its children and never rolls a parent into the human-owned ready lane (never `$READY`). It cites the `leaf-only-lifecycle` rule by slug rather than restating the state machine.
158
158
  - Never post empty or minimal comments — if a milestone has no meaningful content, skip the post.
@@ -728,7 +728,8 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/rollup-blocker-classification.mjs" \
728
728
  report already asks a person to decide.
729
729
 
730
730
  3. **If the derived state differs from the parent's current state, apply it** via the vendor's
731
- lifecycle write (JIRA transition, GitHub/Linear label swap keeping exactly one `status:*`),
731
+ lifecycle write: a JIRA transition; a GitHub label swap keeping exactly one `status:*`; or a
732
+ Linear `save-issue` update that writes the resolved workflow `stateId`, never a lifecycle label.
732
733
  removing any conflicting stale build lifecycle role — **including a stale `ready`** the parent
733
734
  should never carry. Post an idempotent `[lisa-repair-intake]` rollup note naming the derived
734
735
  state and the child tally (honor the backoff window + fingerprint). Include that exact rendered
@@ -131,6 +131,7 @@ export function probe(name, exec = boundedChildOutput) {
131
131
  return { present: true, version: extractVersion(out) };
132
132
  } catch (err) {
133
133
  if (err.code === "ENOENT") return { present: false, version: null };
134
+ if (err.code === "ETIMEDOUT") throw err;
134
135
  const output = `${err.stdout ?? ""}${err.stderr ?? ""}`;
135
136
  return { present: true, version: extractVersion(output) };
136
137
  }
@@ -808,9 +809,9 @@ export function installUserSessionHook(repoRoot, options = {}) {
808
809
  const dir = join(home, ".claude");
809
810
  const settingsPath = join(dir, "settings.json");
810
811
  // Settings stores shell source, so the absolute path still needs quoting.
811
- // JSON string syntax is accepted by the shell for ordinary path text and
812
- // safely escapes spaces, quotes, dollars and backticks.
813
- const command = `bash ${JSON.stringify(script)}`;
812
+ // Single quotes suppress parameter expansion and command substitution. An
813
+ // embedded quote closes, escapes, and reopens the quoted shell word.
814
+ const command = `bash '${script.replaceAll("'", `'\\''`)}'`;
814
815
 
815
816
  let settings = {};
816
817
  if (exists(settingsPath)) {
@@ -37,11 +37,11 @@ Two consequences the vendor arms must not restate differently:
37
37
  Resolve roles through the shared resolver rather than an inlined helper:
38
38
 
39
39
  ```bash
40
- node "${CLAUDE_PLUGIN_ROOT}/scripts/resolve-lifecycle-role.mjs" \
40
+ node "${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-plugins/lisa}}/scripts/resolve-lifecycle-role.mjs" \
41
41
  --role <role> --vendor <jira|linear|github> --intent <read|write> [--env <env>]
42
42
  ```
43
43
 
44
- Exit `0` with a value means configured; exit `0` with **empty** output means an optional role is unset — skip the transition; exit `2` means a required role is unset or a write was refused a fallback value.
44
+ Exit `0` with a value means configured; exit `0` with **empty** output means an optional role is unset — skip the transition; exit `2` means a required role is unset or a write was refused a fallback value. Any other exit is a resolver failure, never an unset role.
45
45
 
46
46
  If `$ARGUMENTS` is empty, all vendor skills auto-detect a ticket reference from the active plan file (most recently modified `.md` in `plans/`).
47
47
 
@@ -82,7 +82,7 @@ export const FAMILIES = [
82
82
  },
83
83
  {
84
84
  id: "lighthouse",
85
- match: /(^|\/)lighthouserc-config\.json$/,
85
+ match: /(^|\/)(?:lighthouserc-config|lighthouserc|\.lighthouserc)\.json$/,
86
86
  kind: "lighthouse",
87
87
  },
88
88
  ];
@@ -101,6 +101,8 @@ export const LIGHTHOUSE_ASSERTION_DIRECTIONS = Object.freeze({
101
101
  maxLength: "max",
102
102
  });
103
103
 
104
+ const LIGHTHOUSE_LEVELS = Object.freeze({ off: 0, warn: 1, error: 2 });
105
+
104
106
  /**
105
107
  * Extract numeric Lighthouse assertions with a direction chosen per key.
106
108
  *
@@ -136,6 +138,13 @@ export function extractLighthouseAssertions(conf) {
136
138
  return out;
137
139
  }
138
140
  for (const [audit, spec] of Object.entries(assertions)) {
141
+ const level = Array.isArray(spec) ? spec[0] : spec;
142
+ if (typeof level === "string" && Object.hasOwn(LIGHTHOUSE_LEVELS, level)) {
143
+ out.set(`${audit}.$level`, {
144
+ value: LIGHTHOUSE_LEVELS[level],
145
+ direction: "min",
146
+ });
147
+ }
139
148
  const options =
140
149
  Array.isArray(spec) &&
141
150
  spec.length >= 2 &&
@@ -2002,7 +2002,7 @@ function bypassParagraph(claim, context) {
2002
2002
  function bypassLabelLines(context) {
2003
2003
  const label = context.bypassLabel;
2004
2004
  const measured = context.bypassLabelState;
2005
- const state = measured?.state ?? BYPASS_LABEL_STATE.unknown;
2005
+ const state = measured?.state ?? BYPASS_LABEL_STATE.notMeasured;
2006
2006
  if (state === BYPASS_LABEL_STATE.absent) {
2007
2007
  return [
2008
2008
  "> [!CAUTION]",
@@ -3377,7 +3377,8 @@ export function resolveSettings(env) {
3377
3377
  freshnessHours: limits.freshnessHours,
3378
3378
  bootstrapUntil: env.NIGHTLY_BOOTSTRAP_UNTIL || "",
3379
3379
  bootstrapMaxDays: limits.bootstrapMaxDays,
3380
- bypassLabel: env.NIGHTLY_BYPASS_LABEL || DEFAULT_BYPASS_LABEL,
3380
+ bypassLabel:
3381
+ String(env.NIGHTLY_BYPASS_LABEL ?? "").trim() || DEFAULT_BYPASS_LABEL,
3381
3382
  bypassMaxHours: limits.bypassMaxHours,
3382
3383
  // An ADDITIONAL project rule, never a replacement — see `evaluateBypass`.
3383
3384
  extraBypassReasonPattern: env.NIGHTLY_BYPASS_REASON_PATTERN || "",
@@ -3550,17 +3551,13 @@ export async function runReport(env, wait) {
3550
3551
  const bypassLabelState =
3551
3552
  requiredness.state === REQUIREDNESS.required
3552
3553
  ? await fetchBypassLabelState(settings.api, settings.bypassLabel, wait)
3553
- : requiredness.state === REQUIREDNESS.notRequired
3554
- ? Object.freeze({
3555
- state: BYPASS_LABEL_STATE.notMeasured,
3556
- detail:
3557
- "not measured — this gate is not required on this branch, so no waiver recipe is printed and a missing label waives nothing",
3558
- })
3559
- : Object.freeze({
3560
- state: BYPASS_LABEL_STATE.unknown,
3561
- detail:
3562
- "not measured — whether this gate is required is unknown, so label state cannot establish whether a waiver recipe applies",
3563
- });
3554
+ : Object.freeze({
3555
+ state: BYPASS_LABEL_STATE.notMeasured,
3556
+ detail:
3557
+ requiredness.state === REQUIREDNESS.notRequired
3558
+ ? "not measured — this gate is not required on this branch, so no waiver recipe is printed and a missing label waives nothing"
3559
+ : "not measured — whether this gate is required could not be read, so the labels API was not queried",
3560
+ });
3564
3561
  const context = {
3565
3562
  branch: settings.branch,
3566
3563
  label: settings.issueLabel,
@@ -188,7 +188,8 @@
188
188
  * content problem and never says the word "permission".
189
189
  *
190
190
  * A refusal is not a finding: it fails, and under `"enforcement": "warn"` it is
191
- * loud and exits 0, matching what an untranscribed snapshot already does.
191
+ * normally loud and exits 0. `--require-review-evidence` is the deliberate
192
+ * exception: a refusal then blocks because the caller required proven evidence.
192
193
  *
193
194
  * ## `--fail-on-vacuous` — the supported exit code
194
195
  *
@@ -1541,18 +1542,48 @@ export function fetchChecksForCommit(sha, repo) {
1541
1542
  );
1542
1543
  const runs = ghApiPaginatedArray(
1543
1544
  `repos/${slug}/commits/${sha}/check-runs?per_page=100`,
1544
- '[.check_runs[] | {name: .name, conclusion: .conclusion, description: (.output.title // "")}]'
1545
+ '[.check_runs[] | {id: .id, name: .name, conclusion: .conclusion, description: (.output.title // ""), completed_at: .completed_at, started_at: .started_at}]'
1545
1546
  );
1546
1547
  return mergeCheckRows(
1547
1548
  statuses.map(row =>
1548
1549
  normalizeCheckRow(row.name, row.state, row.description)
1549
1550
  ),
1550
- runs.map(row =>
1551
+ newestCheckRuns(runs).map(row =>
1551
1552
  normalizeCheckRow(row.name, row.conclusion, row.description)
1552
1553
  )
1553
1554
  );
1554
1555
  }
1555
1556
 
1557
+ /**
1558
+ * Keep the newest check run for each name across every check suite on a SHA.
1559
+ * @param {ReadonlyArray<object>} runs Raw check-run rows with timestamps and ids.
1560
+ * @returns {object[]} One newest row per name.
1561
+ */
1562
+ export function newestCheckRuns(runs) {
1563
+ const newest = new Map();
1564
+ const rank = row => {
1565
+ const timestamp = Date.parse(row.completed_at ?? row.started_at ?? "");
1566
+ return [Number.isFinite(timestamp) ? timestamp : 0, Number(row.id) || 0];
1567
+ };
1568
+ for (const row of runs) {
1569
+ const name = String(row?.name ?? "");
1570
+ const previous = newest.get(name);
1571
+ if (!previous) {
1572
+ newest.set(name, row);
1573
+ continue;
1574
+ }
1575
+ const [rowTime, rowId] = rank(row);
1576
+ const [previousTime, previousId] = rank(previous);
1577
+ if (
1578
+ rowTime > previousTime ||
1579
+ (rowTime === previousTime && rowId > previousId)
1580
+ ) {
1581
+ newest.set(name, row);
1582
+ }
1583
+ }
1584
+ return [...newest.values()];
1585
+ }
1586
+
1556
1587
  /**
1557
1588
  * Collapse GitHub's two reporter APIs into one authoritative row per name.
1558
1589
  *
@@ -184,6 +184,7 @@ export const OUTCOMES = Object.freeze({
184
184
  dryRunTimeout: "mutation-gate: dry-run-timeout",
185
185
  scoreBelowBreak: "mutation-gate: score-below-break",
186
186
  runFailed: "mutation-gate: run-failed",
187
+ invalidMutatePattern: "mutation-gate: invalid-mutate-pattern",
187
188
  childDeadline: "mutation-gate: child-deadline",
188
189
  sandboxReclaimed: "mutation-gate: sandbox-reclaimed",
189
190
  timeoutAccounting: "mutation-gate: timeout-accounting",
@@ -297,6 +298,11 @@ export const globToRegExp = glob => {
297
298
  } else if (char === "," && braceDepth > 0) source += "|";
298
299
  else source += char.replaceAll(/[.+^${}()|[\]\\]/gu, "\\$&");
299
300
  }
301
+ if (braceDepth !== 0) {
302
+ throw new Error(
303
+ `mutate pattern "${glob}" has ${braceDepth} unclosed "{" — fix the brace alternation in your Stryker config.`
304
+ );
305
+ }
300
306
  return new RegExp(`${source}$`, "u");
301
307
  };
302
308
 
@@ -2013,7 +2019,17 @@ export const runGate = (cwd = process.cwd(), argv = []) => {
2013
2019
  }
2014
2020
 
2015
2021
  const declaration = resolveMutateDeclaration(cwd);
2016
- const patterns = compileMutatePatterns(declaration.mutate);
2022
+ let patterns;
2023
+ try {
2024
+ patterns = compileMutatePatterns(declaration.mutate);
2025
+ } catch (error) {
2026
+ console.error(
2027
+ `❌ ${OUTCOMES.invalidMutatePattern}\n` +
2028
+ ` ${error instanceof Error ? error.message : String(error)}\n` +
2029
+ ` Fix the \`mutate\` patterns in ${declaration.source} before re-running.`
2030
+ );
2031
+ return 1;
2032
+ }
2017
2033
 
2018
2034
  if (countMutateTargetsInRepo(cwd, patterns) === 0) {
2019
2035
  console.error(
@@ -82,7 +82,7 @@ export const FAMILIES = [
82
82
  },
83
83
  {
84
84
  id: "lighthouse",
85
- match: /(^|\/)lighthouserc-config\.json$/,
85
+ match: /(^|\/)(?:lighthouserc-config|lighthouserc|\.lighthouserc)\.json$/,
86
86
  kind: "lighthouse",
87
87
  },
88
88
  ];
@@ -101,6 +101,8 @@ export const LIGHTHOUSE_ASSERTION_DIRECTIONS = Object.freeze({
101
101
  maxLength: "max",
102
102
  });
103
103
 
104
+ const LIGHTHOUSE_LEVELS = Object.freeze({ off: 0, warn: 1, error: 2 });
105
+
104
106
  /**
105
107
  * Extract numeric Lighthouse assertions with a direction chosen per key.
106
108
  *
@@ -136,6 +138,13 @@ export function extractLighthouseAssertions(conf) {
136
138
  return out;
137
139
  }
138
140
  for (const [audit, spec] of Object.entries(assertions)) {
141
+ const level = Array.isArray(spec) ? spec[0] : spec;
142
+ if (typeof level === "string" && Object.hasOwn(LIGHTHOUSE_LEVELS, level)) {
143
+ out.set(`${audit}.$level`, {
144
+ value: LIGHTHOUSE_LEVELS[level],
145
+ direction: "min",
146
+ });
147
+ }
139
148
  const options =
140
149
  Array.isArray(spec) &&
141
150
  spec.length >= 2 &&