@codyswann/lisa 4.34.3 → 4.34.5

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 (85) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +40 -1
  2. package/all/copy-overwrite/scripts/lisa-work-item.mjs +14 -1
  3. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.js +8 -0
  5. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +10 -8
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +5 -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/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +35 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +35 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +35 -0
  16. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +134 -0
  17. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +35 -0
  18. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +35 -0
  19. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +35 -0
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +134 -0
  22. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +35 -0
  23. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +35 -0
  24. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +35 -0
  25. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  28. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +134 -0
  32. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +35 -0
  33. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +35 -0
  34. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +35 -0
  35. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +134 -0
  37. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +35 -0
  38. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +35 -0
  39. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +35 -0
  40. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-expo-agy/plugin.json +1 -1
  43. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  63. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  67. package/plugins/lisa-rails-agy/plugin.json +1 -1
  68. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  73. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  77. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  78. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  80. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +134 -0
  81. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +35 -0
  82. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +35 -0
  83. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +35 -0
  84. package/scripts/two-channel-couplings.json +1 -1
  85. package/typescript/copy-overwrite/.claude/hooks/worktree-create.sh +174 -12
package/package.json CHANGED
@@ -127,6 +127,7 @@
127
127
  "resolutions": {
128
128
  "@isaacs/brace-expansion": "^5.0.1",
129
129
  "axios": ">=1.15.2",
130
+ "browserslist": ">=4.28.8",
130
131
  "deepmerge-ts": "^8.0.1",
131
132
  "esbuild": ">=0.28.1",
132
133
  "eslint-plugin-react-hooks": "^7.0.0",
@@ -165,7 +166,7 @@
165
166
  "zod-validation-error": "^4.0.0"
166
167
  },
167
168
  "name": "@codyswann/lisa",
168
- "version": "4.34.3",
169
+ "version": "4.34.5",
169
170
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
170
171
  "main": "dist/index.js",
171
172
  "exports": {
@@ -313,7 +314,7 @@
313
314
  "test": "tests"
314
315
  },
315
316
  "types": "./dist/index.d.ts",
316
- "lisaReleaseCommit": "77f31b7f3b5701b52d158ae63015212acf8be6e4",
317
- "gitHead": "77f31b7f3b5701b52d158ae63015212acf8be6e4",
318
- "lisaReleaseTag": "v4.34.3"
317
+ "lisaReleaseCommit": "f53b310d12af3c0d1e81173e95d9db7c5de23a5c",
318
+ "gitHead": "f53b310d12af3c0d1e81173e95d9db7c5de23a5c",
319
+ "lisaReleaseTag": "v4.34.5"
319
320
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.3",
3
+ "version": "4.34.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.3",
3
+ "version": "4.34.5",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -300,6 +300,41 @@ Lifecycle membership is decided by the **`status:` prefix**, never by a pinned m
300
300
 
301
301
  ### Phase 3 — Process the first eligible ready issue
302
302
 
303
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
304
+
305
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
306
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
307
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
308
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
309
+ matters.
310
+
311
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
312
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
313
+ other gate's verdict — however conclusive — may promote an item a person parked.
314
+
315
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
316
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
317
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
318
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
319
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
320
+ on every item that happens to have one.
321
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
322
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
323
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
324
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
325
+ outside its filter twice over. Call
326
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
327
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
328
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
329
+ already out of the lane and already marked yields no second mutation and no second comment. This
330
+ is the same repair the leaf-only gate already performs for a ready item that must not be
331
+ dispatched.
332
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
333
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
334
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
335
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
336
+
337
+
303
338
  #### 3a.0 Repo-scope gate (claim only current-repo issues)
304
339
 
305
340
  GitHub Issues live in one repo by definition, so the scanned repo's issues are usually inherently current-repo. But a planning/umbrella repo's issues can target sibling repos, so this skill still claims only issues for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -205,6 +205,41 @@ true, and nothing re-read one before this phase. For each pre-work candidate out
205
205
 
206
206
  ### Phase 3 — Process the first eligible ready ticket
207
207
 
208
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
209
+
210
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
211
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
212
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
213
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
214
+ matters.
215
+
216
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
217
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
218
+ other gate's verdict — however conclusive — may promote an item a person parked.
219
+
220
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
221
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
222
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
223
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
224
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
225
+ on every item that happens to have one.
226
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
227
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
228
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
229
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
230
+ outside its filter twice over. Call
231
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
232
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
233
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
234
+ already out of the lane and already marked yields no second mutation and no second comment. This
235
+ is the same repair the leaf-only gate already performs for a ready item that must not be
236
+ dispatched.
237
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
238
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
239
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
240
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
241
+
242
+
208
243
  #### 3a.0 Repo-scope gate (claim only current-repo tickets)
209
244
 
210
245
  A JIRA project can oversee multiple repos (`frontend` / `backend` / `infrastructure`). This skill claims only tickets for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -199,6 +199,41 @@ classifyPreWorkCandidate({ laneType, labels, body, humanNeededLabel, statedBlock
199
199
 
200
200
  ### Phase 3 — Process the first eligible ready Issue
201
201
 
202
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
203
+
204
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
205
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
206
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
207
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
208
+ matters.
209
+
210
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
211
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
212
+ other gate's verdict — however conclusive — may promote an item a person parked.
213
+
214
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
215
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
216
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
217
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
218
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
219
+ on every item that happens to have one.
220
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
221
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
222
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
223
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
224
+ outside its filter twice over. Call
225
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
226
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
227
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
228
+ already out of the lane and already marked yields no second mutation and no second comment. This
229
+ is the same repair the leaf-only gate already performs for a ready item that must not be
230
+ dispatched.
231
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
232
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
233
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
234
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
235
+
236
+
202
237
  #### 3a.0 Repo-scope gate (claim only current-repo Issues)
203
238
 
204
239
  A Linear team can oversee multiple repos (`frontend` / `backend` / `infrastructure`). This skill claims only Issues for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -188,3 +188,137 @@ export function formatReprobeNote(candidate = {}, verdict) {
188
188
 
189
189
  return lines.join("\n");
190
190
  }
191
+
192
+ /**
193
+ * Marker on the note this module leaves, so a later cycle recognises its own
194
+ * work instead of commenting again.
195
+ */
196
+ export const HUMAN_GATE_NOTE_MARKER = "<!-- [lisa-human-gate-reconciled] -->";
197
+
198
+ /**
199
+ * Decide whether a candidate ALREADY IN THE READY LANE may be claimed.
200
+ *
201
+ * `classifyPreWorkCandidate` was the only caller of the human-gate test, and it
202
+ * runs on candidates outside the ready lane. An item that reached the ready
203
+ * lane was therefore claimed with the gate never consulted — the check was
204
+ * correct and simply unreachable from the path that matters.
205
+ *
206
+ * This shares `isHumanGated` with the pre-work classifier deliberately. Two
207
+ * copies of a substring test drift, and a drifted gate fails silently: it stops
208
+ * matching, and nothing reports that it stopped.
209
+ *
210
+ * The test stays a plain literal-substring match and is NOT keyed on `reason=`.
211
+ * Markers in the wild carry no `reason=` at all and sit anywhere in the body, so
212
+ * a parser keyed on the structured field would miss those while appearing to
213
+ * work on every item that happens to have one — reproducing this defect one
214
+ * layer down.
215
+ * @param {{ labels?: unknown, body?: unknown, humanNeededLabel?: unknown }} input - Candidate surfaces
216
+ * @returns {{ claimable: boolean, reason: string, humanGated: boolean }} Claim verdict
217
+ */
218
+ export function classifyReadyCandidate(input = {}) {
219
+ if (isHumanGated(input)) {
220
+ return { claimable: false, reason: "human-gate", humanGated: true };
221
+ }
222
+ return { claimable: true, reason: "ready-eligible", humanGated: false };
223
+ }
224
+
225
+ /**
226
+ * Plan the state repair for a ready-lane item that is held for a person.
227
+ *
228
+ * Skipping such an item is not enough. `lisa-repair-intake` sweeps items that
229
+ * are NOT in the ready role and excludes gated ones outright, so a ready AND
230
+ * gated item falls outside its filter twice over. Skipping alone would leave it
231
+ * re-evaluated and re-rejected every cycle forever, seen by nothing — trading a
232
+ * loud failure for a silent one.
233
+ *
234
+ * So the lane is reconciled to match the hold: the item leaves the pickup queue
235
+ * and gains the human-needed marker. That is the same repair the leaf-only gate
236
+ * already performs for a ready item that must not be dispatched.
237
+ *
238
+ * Idempotent by state rather than by memory: an item already out of the queue
239
+ * and already marked needs nothing, so asking twice yields no second mutation.
240
+ * @param {{
241
+ * labels?: unknown
242
+ * body?: unknown
243
+ * humanNeededLabel?: unknown
244
+ * readyLabel?: unknown
245
+ * alreadyNotified?: unknown
246
+ * }} input - Candidate surfaces plus the configured lane and marker labels
247
+ * @returns {{ gated: boolean, reason: string, actions: { removeReadyRole: boolean, addHumanNeededLabel: boolean, comment: boolean } }} Repair plan
248
+ */
249
+ export function planHumanGateReconciliation(input = {}) {
250
+ const idle = Object.freeze({
251
+ removeReadyRole: false,
252
+ addHumanNeededLabel: false,
253
+ comment: false,
254
+ });
255
+ if (!isHumanGated(input)) {
256
+ return { gated: false, reason: "not-gated", actions: idle };
257
+ }
258
+
259
+ const labels = normalizeLabels(input.labels);
260
+ const needed = String(input.humanNeededLabel ?? DEFAULT_HUMAN_NEEDED_LABEL)
261
+ .trim()
262
+ .toLowerCase();
263
+ const ready = String(input.readyLabel ?? "")
264
+ .trim()
265
+ .toLowerCase();
266
+ const inReadyLane = ready.length > 0 && labels.includes(ready);
267
+ const marked = needed.length > 0 && labels.includes(needed);
268
+ const actions = {
269
+ removeReadyRole: inReadyLane,
270
+ addHumanNeededLabel: needed.length > 0 && !marked,
271
+ comment:
272
+ input.alreadyNotified !== true &&
273
+ (inReadyLane || (needed.length > 0 && !marked)),
274
+ };
275
+ const changed =
276
+ actions.removeReadyRole || actions.addHumanNeededLabel || actions.comment;
277
+
278
+ return {
279
+ gated: true,
280
+ reason: changed ? "reconcile" : "already-reconciled",
281
+ actions,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Render the hold notice for a reader who does not know this subsystem.
287
+ *
288
+ * Written for a non-technical operator on purpose — that is who stands at this
289
+ * gate. It names what was found, what changed, and how to undo it, and uses no
290
+ * skill name, label syntax, or phase number, none of which a reader can act on.
291
+ * @returns {string} Comment body, carrying the marker that keeps a re-run quiet
292
+ */
293
+ export function formatHumanGateNote() {
294
+ return [
295
+ "**On hold for a person**",
296
+ "",
297
+ "- What was found: this item is marked as held for a person, but it was " +
298
+ "also sitting in the queue that agents build from — so it would have " +
299
+ "been picked up and built anyway.",
300
+ "- What changed: it has been taken out of that queue and flagged as " +
301
+ "needing a person, so nothing will build it automatically.",
302
+ "- To resume it: remove the hold note from the description and put it " +
303
+ "back in the build queue.",
304
+ "",
305
+ HUMAN_GATE_NOTE_MARKER,
306
+ ].join("\n");
307
+ }
308
+
309
+ /**
310
+ * Render the cycle-summary line naming what was held.
311
+ *
312
+ * A lane mutation nobody can see afterwards is the same class of problem as the
313
+ * one this fixes, so the run says what it moved — and it keeps "nothing was
314
+ * eligible" distinguishable from "something eligible was held".
315
+ * @param {readonly unknown[]} held - Item references held this cycle
316
+ * @returns {string} One summary line
317
+ */
318
+ export function summarizeHumanGateHolds(held = []) {
319
+ const names = (Array.isArray(held) ? held : [])
320
+ .map(item => trimmedString(item))
321
+ .filter(name => name.length > 0);
322
+ if (names.length === 0) return "Held for a person: none.";
323
+ return `Held for a person (${String(names.length)}): ${names.join(", ")}.`;
324
+ }
@@ -300,6 +300,41 @@ Lifecycle membership is decided by the **`status:` prefix**, never by a pinned m
300
300
 
301
301
  ### Phase 3 — Process the first eligible ready issue
302
302
 
303
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
304
+
305
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
306
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
307
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
308
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
309
+ matters.
310
+
311
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
312
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
313
+ other gate's verdict — however conclusive — may promote an item a person parked.
314
+
315
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
316
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
317
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
318
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
319
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
320
+ on every item that happens to have one.
321
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
322
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
323
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
324
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
325
+ outside its filter twice over. Call
326
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
327
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
328
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
329
+ already out of the lane and already marked yields no second mutation and no second comment. This
330
+ is the same repair the leaf-only gate already performs for a ready item that must not be
331
+ dispatched.
332
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
333
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
334
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
335
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
336
+
337
+
303
338
  #### 3a.0 Repo-scope gate (claim only current-repo issues)
304
339
 
305
340
  GitHub Issues live in one repo by definition, so the scanned repo's issues are usually inherently current-repo. But a planning/umbrella repo's issues can target sibling repos, so this skill still claims only issues for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -205,6 +205,41 @@ true, and nothing re-read one before this phase. For each pre-work candidate out
205
205
 
206
206
  ### Phase 3 — Process the first eligible ready ticket
207
207
 
208
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
209
+
210
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
211
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
212
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
213
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
214
+ matters.
215
+
216
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
217
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
218
+ other gate's verdict — however conclusive — may promote an item a person parked.
219
+
220
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
221
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
222
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
223
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
224
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
225
+ on every item that happens to have one.
226
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
227
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
228
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
229
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
230
+ outside its filter twice over. Call
231
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
232
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
233
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
234
+ already out of the lane and already marked yields no second mutation and no second comment. This
235
+ is the same repair the leaf-only gate already performs for a ready item that must not be
236
+ dispatched.
237
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
238
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
239
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
240
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
241
+
242
+
208
243
  #### 3a.0 Repo-scope gate (claim only current-repo tickets)
209
244
 
210
245
  A JIRA project can oversee multiple repos (`frontend` / `backend` / `infrastructure`). This skill claims only tickets for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -199,6 +199,41 @@ classifyPreWorkCandidate({ laneType, labels, body, humanNeededLabel, statedBlock
199
199
 
200
200
  ### Phase 3 — Process the first eligible ready Issue
201
201
 
202
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
203
+
204
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
205
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
206
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
207
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
208
+ matters.
209
+
210
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
211
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
212
+ other gate's verdict — however conclusive — may promote an item a person parked.
213
+
214
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
215
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
216
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
217
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
218
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
219
+ on every item that happens to have one.
220
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
221
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
222
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
223
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
224
+ outside its filter twice over. Call
225
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
226
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
227
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
228
+ already out of the lane and already marked yields no second mutation and no second comment. This
229
+ is the same repair the leaf-only gate already performs for a ready item that must not be
230
+ dispatched.
231
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
232
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
233
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
234
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
235
+
236
+
202
237
  #### 3a.0 Repo-scope gate (claim only current-repo Issues)
203
238
 
204
239
  A Linear team can oversee multiple repos (`frontend` / `backend` / `infrastructure`). This skill claims only Issues for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.34.3",
3
+ "version": "4.34.5",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -188,3 +188,137 @@ export function formatReprobeNote(candidate = {}, verdict) {
188
188
 
189
189
  return lines.join("\n");
190
190
  }
191
+
192
+ /**
193
+ * Marker on the note this module leaves, so a later cycle recognises its own
194
+ * work instead of commenting again.
195
+ */
196
+ export const HUMAN_GATE_NOTE_MARKER = "<!-- [lisa-human-gate-reconciled] -->";
197
+
198
+ /**
199
+ * Decide whether a candidate ALREADY IN THE READY LANE may be claimed.
200
+ *
201
+ * `classifyPreWorkCandidate` was the only caller of the human-gate test, and it
202
+ * runs on candidates outside the ready lane. An item that reached the ready
203
+ * lane was therefore claimed with the gate never consulted — the check was
204
+ * correct and simply unreachable from the path that matters.
205
+ *
206
+ * This shares `isHumanGated` with the pre-work classifier deliberately. Two
207
+ * copies of a substring test drift, and a drifted gate fails silently: it stops
208
+ * matching, and nothing reports that it stopped.
209
+ *
210
+ * The test stays a plain literal-substring match and is NOT keyed on `reason=`.
211
+ * Markers in the wild carry no `reason=` at all and sit anywhere in the body, so
212
+ * a parser keyed on the structured field would miss those while appearing to
213
+ * work on every item that happens to have one — reproducing this defect one
214
+ * layer down.
215
+ * @param {{ labels?: unknown, body?: unknown, humanNeededLabel?: unknown }} input - Candidate surfaces
216
+ * @returns {{ claimable: boolean, reason: string, humanGated: boolean }} Claim verdict
217
+ */
218
+ export function classifyReadyCandidate(input = {}) {
219
+ if (isHumanGated(input)) {
220
+ return { claimable: false, reason: "human-gate", humanGated: true };
221
+ }
222
+ return { claimable: true, reason: "ready-eligible", humanGated: false };
223
+ }
224
+
225
+ /**
226
+ * Plan the state repair for a ready-lane item that is held for a person.
227
+ *
228
+ * Skipping such an item is not enough. `lisa-repair-intake` sweeps items that
229
+ * are NOT in the ready role and excludes gated ones outright, so a ready AND
230
+ * gated item falls outside its filter twice over. Skipping alone would leave it
231
+ * re-evaluated and re-rejected every cycle forever, seen by nothing — trading a
232
+ * loud failure for a silent one.
233
+ *
234
+ * So the lane is reconciled to match the hold: the item leaves the pickup queue
235
+ * and gains the human-needed marker. That is the same repair the leaf-only gate
236
+ * already performs for a ready item that must not be dispatched.
237
+ *
238
+ * Idempotent by state rather than by memory: an item already out of the queue
239
+ * and already marked needs nothing, so asking twice yields no second mutation.
240
+ * @param {{
241
+ * labels?: unknown
242
+ * body?: unknown
243
+ * humanNeededLabel?: unknown
244
+ * readyLabel?: unknown
245
+ * alreadyNotified?: unknown
246
+ * }} input - Candidate surfaces plus the configured lane and marker labels
247
+ * @returns {{ gated: boolean, reason: string, actions: { removeReadyRole: boolean, addHumanNeededLabel: boolean, comment: boolean } }} Repair plan
248
+ */
249
+ export function planHumanGateReconciliation(input = {}) {
250
+ const idle = Object.freeze({
251
+ removeReadyRole: false,
252
+ addHumanNeededLabel: false,
253
+ comment: false,
254
+ });
255
+ if (!isHumanGated(input)) {
256
+ return { gated: false, reason: "not-gated", actions: idle };
257
+ }
258
+
259
+ const labels = normalizeLabels(input.labels);
260
+ const needed = String(input.humanNeededLabel ?? DEFAULT_HUMAN_NEEDED_LABEL)
261
+ .trim()
262
+ .toLowerCase();
263
+ const ready = String(input.readyLabel ?? "")
264
+ .trim()
265
+ .toLowerCase();
266
+ const inReadyLane = ready.length > 0 && labels.includes(ready);
267
+ const marked = needed.length > 0 && labels.includes(needed);
268
+ const actions = {
269
+ removeReadyRole: inReadyLane,
270
+ addHumanNeededLabel: needed.length > 0 && !marked,
271
+ comment:
272
+ input.alreadyNotified !== true &&
273
+ (inReadyLane || (needed.length > 0 && !marked)),
274
+ };
275
+ const changed =
276
+ actions.removeReadyRole || actions.addHumanNeededLabel || actions.comment;
277
+
278
+ return {
279
+ gated: true,
280
+ reason: changed ? "reconcile" : "already-reconciled",
281
+ actions,
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Render the hold notice for a reader who does not know this subsystem.
287
+ *
288
+ * Written for a non-technical operator on purpose — that is who stands at this
289
+ * gate. It names what was found, what changed, and how to undo it, and uses no
290
+ * skill name, label syntax, or phase number, none of which a reader can act on.
291
+ * @returns {string} Comment body, carrying the marker that keeps a re-run quiet
292
+ */
293
+ export function formatHumanGateNote() {
294
+ return [
295
+ "**On hold for a person**",
296
+ "",
297
+ "- What was found: this item is marked as held for a person, but it was " +
298
+ "also sitting in the queue that agents build from — so it would have " +
299
+ "been picked up and built anyway.",
300
+ "- What changed: it has been taken out of that queue and flagged as " +
301
+ "needing a person, so nothing will build it automatically.",
302
+ "- To resume it: remove the hold note from the description and put it " +
303
+ "back in the build queue.",
304
+ "",
305
+ HUMAN_GATE_NOTE_MARKER,
306
+ ].join("\n");
307
+ }
308
+
309
+ /**
310
+ * Render the cycle-summary line naming what was held.
311
+ *
312
+ * A lane mutation nobody can see afterwards is the same class of problem as the
313
+ * one this fixes, so the run says what it moved — and it keeps "nothing was
314
+ * eligible" distinguishable from "something eligible was held".
315
+ * @param {readonly unknown[]} held - Item references held this cycle
316
+ * @returns {string} One summary line
317
+ */
318
+ export function summarizeHumanGateHolds(held = []) {
319
+ const names = (Array.isArray(held) ? held : [])
320
+ .map(item => trimmedString(item))
321
+ .filter(name => name.length > 0);
322
+ if (names.length === 0) return "Held for a person: none.";
323
+ return `Held for a person (${String(names.length)}): ${names.join(", ")}.`;
324
+ }
@@ -300,6 +300,41 @@ Lifecycle membership is decided by the **`status:` prefix**, never by a pinned m
300
300
 
301
301
  ### Phase 3 — Process the first eligible ready issue
302
302
 
303
+ #### 3.0 Human-hold gate (absolute, and it runs before every other gate)
304
+
305
+ A person parks an item by putting `[lisa-human-gate]` in its description. That marker used to be
306
+ read only for candidates **outside** `$READY` (Phase 2's blocker re-probe), so an item already in
307
+ the ready lane was claimed with the hold never consulted — one was dispatched and fully implemented
308
+ before a human vetoed the merge. The check was correct; it was unreachable from the path that
309
+ matters.
310
+
311
+ Run this **first**, ahead of the repo-scope gate (3a.0) and the leaf-only gate (3a), for every ready
312
+ candidate. Ordering is load-bearing for the same reason it is inside `classifyPreWorkCandidate`: no
313
+ other gate's verdict — however conclusive — may promote an item a person parked.
314
+
315
+ 1. **Classify with `classifyReadyCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. It
316
+ shares the gate test with the pre-work classifier deliberately — two copies of a substring test
317
+ drift, and a drifted gate fails *silently*, by quietly ceasing to match. Do **not** re-implement
318
+ the test here, and do **not** key it on `reason=`: markers in the wild carry no `reason=` key at
319
+ all and sit anywhere in the body, so a structured parse would miss them while appearing to work
320
+ on every item that happens to have one.
321
+ 2. **On `claimable: false` with reason `human-gate`, do not claim and do not dispatch.**
322
+ 3. **Reconcile the lane; do not merely skip.** Skipping alone leaves the item in `$READY`, re-judged
323
+ and re-rejected every cycle forever and seen by nothing — `lisa-repair-intake` sweeps items that
324
+ are **not** in the ready role and excludes gated ones outright, so a ready-and-gated item falls
325
+ outside its filter twice over. Call
326
+ `planHumanGateReconciliation({ labels, body, humanNeededLabel, readyLabel, alreadyNotified })`
327
+ and apply exactly the actions it returns: remove `$READY`, add the configured human-needed
328
+ marker, and post `formatHumanGateNote()` once. The planner is idempotent by state, so an item
329
+ already out of the lane and already marked yields no second mutation and no second comment. This
330
+ is the same repair the leaf-only gate already performs for a ready item that must not be
331
+ dispatched.
332
+ 4. **Name it in the cycle summary** via `summarizeHumanGateHolds([...])`, so the record
333
+ distinguishes "nothing was eligible" from "something eligible was held for a person". A lane
334
+ mutation nobody can see afterwards is the same class of problem this gate exists to fix.
335
+ 5. **Continue to the next candidate.** A held item does not end the cycle.
336
+
337
+
303
338
  #### 3a.0 Repo-scope gate (claim only current-repo issues)
304
339
 
305
340
  GitHub Issues live in one repo by definition, so the scanned repo's issues are usually inherently current-repo. But a planning/umbrella repo's issues can target sibling repos, so this skill still claims only issues for the repo it is running in. Run this gate **before** the leaf-only gate (3a) and the claim (3b), per the `repo-scope-split` rule's "Claim-time repo scoping" section (cite it by slug; do not restate its decision table).