@codyswann/lisa 4.83.1 → 4.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/cli/lisa-test-run-jest-merge.d.ts.map +1 -1
  2. package/dist/cli/lisa-test-run-jest-merge.js +52 -2
  3. package/dist/cli/lisa-test-run-jest-merge.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  5. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  6. package/dist/core/upstream-evidence-manifest.js +17 -12
  7. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  8. package/package.json +4 -4
  9. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  12. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +39 -0
  13. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +6 -0
  14. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  15. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  16. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +1 -0
  18. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  19. package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +8 -1
  21. package/plugins/lisa/rules/reference/config-resolution.md +1 -0
  22. package/plugins/lisa/scripts/ready-queue-order.mjs +109 -6
  23. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  24. package/plugins/lisa/skills/lisa-session-manager/SKILL.md +39 -0
  25. package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +6 -0
  26. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  27. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  28. package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  29. package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +1 -0
  30. package/plugins/lisa/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  31. package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  32. package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +8 -1
  33. package/plugins/lisa-agy/plugin.json +1 -1
  34. package/plugins/lisa-agy/scripts/ready-queue-order.mjs +109 -6
  35. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  36. package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +39 -0
  37. package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +6 -0
  38. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  39. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  40. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  41. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +1 -0
  42. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  43. package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  44. package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +8 -1
  45. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  48. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  52. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
  54. package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +109 -6
  55. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  56. package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +39 -0
  57. package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +6 -0
  58. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  59. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  60. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  61. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +1 -0
  62. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  63. package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  64. package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +8 -1
  65. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
  67. package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +109 -6
  68. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  69. package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +39 -0
  70. package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +6 -0
  71. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  72. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  73. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  74. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +1 -0
  75. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  76. package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  77. package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +8 -1
  78. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  80. package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
  81. package/plugins/lisa-devin/scripts/ready-queue-order.mjs +109 -6
  82. package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  83. package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +39 -0
  84. package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +6 -0
  85. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  86. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  87. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  88. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +1 -0
  89. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  90. package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  91. package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +8 -1
  92. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  93. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo-agy/plugin.json +1 -1
  95. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  106. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  108. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  109. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  110. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  111. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  113. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  116. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  120. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  121. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  122. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  123. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  125. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  126. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  127. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  129. package/plugins/lisa-rails-agy/plugin.json +1 -1
  130. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  131. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  133. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  134. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  135. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  136. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  137. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  138. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  139. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  140. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  141. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  142. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  143. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  144. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  145. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  146. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  147. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  148. package/plugins/src/base/rules/reference/config-resolution.md +1 -0
  149. package/plugins/src/base/scripts/ready-queue-order.mjs +109 -6
  150. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
  151. package/plugins/src/base/skills/lisa-session-manager/SKILL.md +39 -0
  152. package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +6 -0
  153. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
  154. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
  155. package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
  156. package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +1 -0
  157. package/plugins/src/base/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
  158. package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
  159. package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +8 -1
@@ -81,6 +81,17 @@ const FINISH_STATUSES = new Set(["DONE", "FAILED"]);
81
81
  * @property {number} version - Shape version
82
82
  * @property {Record<string, LedgerItem>} items - Items keyed by ref
83
83
  * @property {{paused: boolean, maxWorkers: number|null, me: string|null}} control - Operator controls
84
+ * @property {Record<string, ValidationFailure>} validationFailures - Backlog
85
+ * items that failed validation, keyed by ref, so an unchanged item is not
86
+ * re-validated every cycle
87
+ */
88
+
89
+ /**
90
+ * A backlog item's last failed validation.
91
+ * @typedef {object} ValidationFailure
92
+ * @property {string} updatedAt - The item's tracker `updatedAt` when it failed
93
+ * @property {string} at - When the failure was recorded (ISO)
94
+ * @property {string|null} reason - Short summary of the failing gates
84
95
  */
85
96
 
86
97
  /**
@@ -119,6 +130,7 @@ function emptyLedger() {
119
130
  version: LEDGER_VERSION,
120
131
  items: {},
121
132
  control: { paused: false, maxWorkers: null, me: null },
133
+ validationFailures: {},
122
134
  };
123
135
  }
124
136
 
@@ -159,6 +171,7 @@ export async function readLedger(projectRoot, seams) {
159
171
  version: parsed.version ?? empty.version,
160
172
  items: parsed.items ?? {},
161
173
  control: { ...empty.control, ...parsed.control },
174
+ validationFailures: parsed.validationFailures ?? {},
162
175
  };
163
176
  }
164
177
 
@@ -384,6 +397,63 @@ export function recordFinish(projectRoot, { ref, status, reason }, seams) {
384
397
  });
385
398
  }
386
399
 
400
+ /**
401
+ * Remember that a backlog item failed validation at its current tracker
402
+ * `updatedAt`. The backlog queue skips it until the item changes, so an
403
+ * unready item costs one validation per edit, not one per cycle.
404
+ * @param {string} projectRoot - Absolute project root
405
+ * @param {{ref: string, updatedAt: string, reason?: string|null}} failure - What failed
406
+ * @param {LedgerSeams} [seams] - Injected seams
407
+ * @returns {Promise<ValidationFailure>} The stored record
408
+ */
409
+ export function recordValidationFailure(
410
+ projectRoot,
411
+ { ref, updatedAt, reason = null },
412
+ seams
413
+ ) {
414
+ if (typeof ref !== "string" || ref === "") {
415
+ return Promise.reject(new Error("validation failure needs a ref"));
416
+ }
417
+ if (typeof updatedAt !== "string" || updatedAt === "") {
418
+ return Promise.reject(
419
+ new Error(
420
+ "validation failure needs the item's updatedAt; without it the cache could never expire"
421
+ )
422
+ );
423
+ }
424
+ return updateLedger(projectRoot, seams, (ledger, at) => {
425
+ const entry = { updatedAt, at, reason: reason || null };
426
+ return {
427
+ ledger: {
428
+ ...ledger,
429
+ validationFailures: { ...ledger.validationFailures, [ref]: entry },
430
+ },
431
+ result: entry,
432
+ };
433
+ });
434
+ }
435
+
436
+ /**
437
+ * Forget a backlog item's validation failure (it was promoted, or a human
438
+ * asked for it to be judged again).
439
+ * @param {string} projectRoot - Absolute project root
440
+ * @param {{ref: string}} target - Item to forget
441
+ * @param {LedgerSeams} [seams] - Injected seams
442
+ * @returns {Promise<{cleared: boolean}>} Whether a record existed
443
+ */
444
+ export function clearValidationFailure(projectRoot, { ref }, seams) {
445
+ return updateLedger(projectRoot, seams, ledger => {
446
+ if (!Object.hasOwn(ledger.validationFailures, ref)) {
447
+ return { ledger, result: { cleared: false } };
448
+ }
449
+ const { [ref]: _removed, ...rest } = ledger.validationFailures;
450
+ return {
451
+ ledger: { ...ledger, validationFailures: rest },
452
+ result: { cleared: true },
453
+ };
454
+ });
455
+ }
456
+
387
457
  /**
388
458
  * Check a control patch before it is written.
389
459
  * @param {{paused?: unknown, maxWorkers?: unknown, me?: unknown}} patch - Patch
@@ -130,6 +130,8 @@ const LEDGER_SPECS = {
130
130
  },
131
131
  "clear-question": { options: {}, positionals: 1 },
132
132
  finish: { options: { status: text, sender: text }, positionals: 1 },
133
+ "validation-failed": { options: { "updated-at": text }, positionals: 1 },
134
+ "validation-cleared": { options: {}, positionals: 1 },
133
135
  control: {
134
136
  options: { paused: text, "max-workers": text, me: text },
135
137
  positionals: 0,
@@ -68,7 +68,14 @@ writes `$SM`, and pass `--project-root "$ROOT"` (the absolute project root) on e
68
68
  "me", spelled as the tracker API returns it: the GitHub **login** (`gh api users/<login> --jq
69
69
  .login`), the Jira `accountId`, or the Linear user id (the operator's entry in the team's member
70
70
  list, or `viewer.id` only when the operator runs the lookup with their own credential).
71
- 5. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
71
+ 5. Ask whether to set `sessionManager.backlogPickup` to `true` (default off). When the Ready queue
72
+ is exhausted and a worker slot is still free, the manager then looks at backlog items (open
73
+ leaves in no lifecycle role) that nobody else is assigned to, that are not human-held and have
74
+ no unfinished blocker. Each one that passes the full tracker validation, including the
75
+ stateless-pickup check, is assigned to the first `operatorIds` entry, moved to Ready and
76
+ dispatched. Say plainly that this lets the manager start work nobody marked Ready, so it needs
77
+ `operatorIds` and should go with `trustedAuthorIds` on any tracker outsiders can write to.
78
+ 6. `node "$SM/session-manager.mjs" validate --project-root "$ROOT"` must print `valid: true`.
72
79
 
73
80
  ## Step 3: Store the bot token
74
81
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.83.1",
3
+ "version": "4.84.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -740,6 +740,7 @@ have a `.lisa.config.json` and their own `.git` (written as `"root": "."`).
740
740
  | `sessionManager.notify.stateDir` | **yes** | — | This project's own Telegram state dir. Never the shared default `~/.claude/channels/telegram`, because one bot per state dir is how projects stay isolated. Setup writes `~/.claude/channels/telegram-<project>`. |
741
741
  | `sessionManager.trustedAuthorIds` | no | — | Optional list of **stable tracker user ids**, as strings: the GitHub numeric user id (`gh api users/<login> --jq .id`), the Jira `accountId`, or the Linear user id. Not logins or display names: matching compares ids, so a login silently matches no one and excludes every item. When set, the manager takes only items authored by these people. |
742
742
  | `sessionManager.operatorIds` | no | — | Optional, non-empty list of the people the manager works for, as tracker user ids matched against the **assignee** the same way "me" is: the GitHub **login**, the Jira `accountId`, or the Linear user id, spelled exactly as the tracker API returns it (matching is case-sensitive, so look the OPERATOR up rather than typing from memory: `gh api users/<operator-login> --jq .login` returns the canonical GitHub spelling. Do not use `gh api user`, which returns whoever the credential belongs to — the agent account, when that is the case this key exists for). Dispatch order is assigned to an operator, then to "me" (the tracker credential's identity), then unassigned; items assigned to anyone else are skipped. Set it whenever the tracker credential is not the operator's own (an agent account), or the operator's Ready items are never dispatched. |
743
+ | `sessionManager.backlogPickup` | no | `false` | Opt-in lowest dispatch tier. When the Ready queue is exhausted and capacity remains, the manager considers backlog items (open leaves in no lifecycle role) with the Ready exclusions plus any unfinished blocker; each one that passes the tracker validator (all gates, including the stateless-pickup dry-run) is assigned to the first `operatorIds` entry, moved to Ready in one write, read back and dispatched. A failed validation is cached against the item's `updatedAt`, so it is re-judged only after the item changes. Requires `operatorIds`. |
743
744
 
744
745
  - **Non-secret only.** The bot token lives in `<stateDir>/.env` (directory 0700, file 0600),
745
746
  written by setup. A credential-named field or a token-shaped value anywhere in this block is a
@@ -29,6 +29,12 @@
29
29
  *
30
30
  * Held items are judged by {@link isHumanGated}, the one shared answer to "is
31
31
  * a person holding this?", rather than by a local label check (#3805).
32
+ *
33
+ * {@link orderBacklogQueue} ranks a second, lower tier: open leaf items in no
34
+ * lifecycle role, which the session manager may promote to Ready when
35
+ * `sessionManager.backlogPickup` is on. It applies every Ready exclusion plus
36
+ * two of its own (an unfinished blocker, an unchanged validation failure), and
37
+ * its list is always walked after the whole Ready queue.
32
38
  * @module ready-queue-order
33
39
  */
34
40
 
@@ -67,6 +73,9 @@ const LINEAR_NO_PRIORITY = 4;
67
73
  /** Linear workflow state types that leave a child finished. */
68
74
  const LINEAR_FINISHED_STATE_TYPES = new Set(["completed", "canceled"]);
69
75
 
76
+ /** Jira link types whose inward side reads as "this item is blocked by". */
77
+ const JIRA_BLOCKED_BY = /blocked by/i;
78
+
70
79
  /**
71
80
  * @param {unknown} value
72
81
  * @returns {string}
@@ -88,6 +97,26 @@ function idOrNull(value) {
88
97
  return Number.isSafeInteger(value) ? String(value) : null;
89
98
  }
90
99
 
100
+ /**
101
+ * A non-negative integer count, 0 for anything else.
102
+ * @param {unknown} value
103
+ * @returns {number}
104
+ */
105
+ function count(value) {
106
+ const number = Number(value);
107
+ return Number.isInteger(number) && number > 0 ? number : 0;
108
+ }
109
+
110
+ /**
111
+ * The rows of an array or a GraphQL `{nodes}` connection.
112
+ * @param {unknown} list
113
+ * @returns {any[]}
114
+ */
115
+ function nodesOf(list) {
116
+ if (Array.isArray(list)) return list;
117
+ return Array.isArray(list?.nodes) ? list.nodes : [];
118
+ }
119
+
91
120
  /**
92
121
  * Label names from strings, `{name}` objects or a GraphQL `{nodes}` connection.
93
122
  * @param {unknown} labels
@@ -160,11 +189,14 @@ export function normalizeGithubIssue(issue, options = {}) {
160
189
  assigneeId: idOrNull(assignee?.login),
161
190
  priorityRank: ranks.length > 0 ? Math.min(...ranks) : GITHUB_NO_PRIORITY,
162
191
  createdAt: text(issue.created_at),
192
+ updatedAt: text(issue.updated_at),
163
193
  labels,
164
194
  body: text(issue.body),
165
195
  // Authors are trusted by stable id, never by login (ready-role-filing).
166
196
  authorId: idOrNull(issue.user?.id),
167
197
  isContainer: Number(summary?.total ?? 0) > Number(summary?.completed ?? 0),
198
+ // `blocked_by` in the dependency summary counts only OPEN blockers.
199
+ openBlockers: count(issue.issue_dependencies_summary?.blocked_by),
168
200
  });
169
201
  }
170
202
 
@@ -182,6 +214,12 @@ export function normalizeJiraIssue(issue) {
182
214
  const openSubtasks = (Array.isArray(fields.subtasks) ? fields.subtasks : [])
183
215
  .map(sub => sub?.fields?.status?.statusCategory?.key)
184
216
  .filter(category => category !== "done");
217
+ const openBlockers = nodesOf(fields.issuelinks).filter(
218
+ link =>
219
+ link?.inwardIssue &&
220
+ JIRA_BLOCKED_BY.test(text(link.type?.inward)) &&
221
+ link.inwardIssue.fields?.status?.statusCategory?.key !== "done"
222
+ ).length;
185
223
 
186
224
  return shape({
187
225
  ref: text(issue?.key),
@@ -189,6 +227,7 @@ export function normalizeJiraIssue(issue) {
189
227
  assigneeId: idOrNull(fields.assignee?.accountId),
190
228
  priorityRank: JIRA_PRIORITY_RANKS[priority] ?? JIRA_UNKNOWN_PRIORITY,
191
229
  createdAt: text(fields.created),
230
+ updatedAt: text(fields.updated),
192
231
  labels,
193
232
  body: adfText(fields.description),
194
233
  authorId: idOrNull(fields.reporter?.accountId ?? fields.creator?.accountId),
@@ -196,6 +235,7 @@ export function normalizeJiraIssue(issue) {
196
235
  Number(type.hierarchyLevel ?? 0) > 0 ||
197
236
  text(type.name).toLowerCase() === "epic" ||
198
237
  openSubtasks.length > 0,
238
+ openBlockers,
199
239
  });
200
240
  }
201
241
 
@@ -213,6 +253,13 @@ export function normalizeLinearIssue(issue) {
213
253
  ? issue.children
214
254
  : [];
215
255
  const labels = labelNames(issue?.labels);
256
+ // Linear has no "blocked by" relation type: B blocked by A is A's `blocks`
257
+ // relation, read from B as an inverse relation.
258
+ const openBlockers = nodesOf(issue?.inverseRelations).filter(
259
+ relation =>
260
+ relation?.type === "blocks" &&
261
+ !LINEAR_FINISHED_STATE_TYPES.has(relation.issue?.state?.type)
262
+ ).length;
216
263
 
217
264
  return shape({
218
265
  ref: text(issue?.identifier),
@@ -223,18 +270,22 @@ export function normalizeLinearIssue(issue) {
223
270
  ? native - 1
224
271
  : LINEAR_NO_PRIORITY,
225
272
  createdAt: text(issue?.createdAt),
273
+ updatedAt: text(issue?.updatedAt),
226
274
  labels,
227
275
  body: text(issue?.description),
228
276
  authorId: idOrNull(issue?.creator?.id),
229
277
  isContainer: children.some(
230
278
  child => !LINEAR_FINISHED_STATE_TYPES.has(child?.state?.type)
231
279
  ),
280
+ openBlockers,
232
281
  });
233
282
  }
234
283
 
235
284
  /**
236
285
  * The one item shape every normalizer returns, in a fixed key order.
237
- * @param {{ ref: string, title: string, assigneeId: string | null, priorityRank: number, createdAt: string, labels: string[], body: string, authorId: string | null, isContainer: boolean }} input
286
+ * `updatedAt` keys the backlog tier's validation cache; `openBlockers` counts
287
+ * dependencies whose blocker is not finished. The Ready tier ignores both.
288
+ * @param {{ ref: string, title: string, assigneeId: string | null, priorityRank: number, createdAt: string, updatedAt: string, labels: string[], body: string, authorId: string | null, isContainer: boolean, openBlockers: number }} input
238
289
  */
239
290
  function shape(input) {
240
291
  return {
@@ -243,10 +294,12 @@ function shape(input) {
243
294
  assigneeId: input.assigneeId,
244
295
  priorityRank: input.priorityRank,
245
296
  createdAt: input.createdAt,
297
+ updatedAt: input.updatedAt,
246
298
  labels: input.labels,
247
299
  body: input.body,
248
300
  authorId: input.authorId,
249
301
  isContainer: input.isContainer,
302
+ openBlockers: input.openBlockers,
250
303
  repoLabels: repoLabelsOf(input.labels),
251
304
  };
252
305
  }
@@ -320,7 +373,50 @@ function compareWithinGroup(left, right) {
320
373
  * @returns {{ ordered: Record<string, any>[], excluded: { ref: string, reason: string }[] }}
321
374
  */
322
375
  export function orderReadyQueue(items, options = {}) {
323
- const context = {
376
+ const context = orderingContext(options);
377
+ return rank(items, item => exclusionReason(item, context), context);
378
+ }
379
+
380
+ /**
381
+ * Order normalized backlog items — open leaves in no lifecycle role — for
382
+ * promotion, the tier walked only after the whole Ready queue.
383
+ *
384
+ * Every Ready exclusion applies, then two more: an item with an unfinished
385
+ * blocker (`blocked-by-open-item`) cannot be taken to completion, and an item
386
+ * that already failed validation at its current `updatedAt`
387
+ * (`failed-validation-unchanged`, from `validationFailures`) would only fail
388
+ * again, so it waits until someone edits it. Grouping and ordering match
389
+ * {@link orderReadyQueue}.
390
+ * @param {readonly Record<string, any>[]} items - Normalized items
391
+ * @param {{ me?: string | null, operators?: readonly unknown[], trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string, validationFailures?: Readonly<Record<string, { updatedAt?: string }>> }} [options]
392
+ * @returns {{ ordered: Record<string, any>[], excluded: { ref: string, reason: string }[] }}
393
+ */
394
+ export function orderBacklogQueue(items, options = {}) {
395
+ const context = orderingContext(options);
396
+ const failures = options.validationFailures ?? {};
397
+ return rank(
398
+ items,
399
+ item => {
400
+ const reason = exclusionReason(item, context);
401
+ if (reason !== null) return reason;
402
+ if (count(item.openBlockers) > 0) return "blocked-by-open-item";
403
+ const failed = Object.hasOwn(failures, item.ref)
404
+ ? failures[item.ref]
405
+ : null;
406
+ return failed && text(failed.updatedAt) === text(item.updatedAt)
407
+ ? "failed-validation-unchanged"
408
+ : null;
409
+ },
410
+ context
411
+ );
412
+ }
413
+
414
+ /**
415
+ * The identities and policy every ordering judges against.
416
+ * @param {{ me?: string | null, operators?: readonly unknown[], trustedAuthorIds?: readonly unknown[], humanNeededLabel?: string }} options
417
+ */
418
+ function orderingContext(options) {
419
+ return {
324
420
  me: idOrNull(options.me),
325
421
  operators: new Set((options.operators ?? []).map(idOrNull).filter(Boolean)),
326
422
  trusted: new Set(
@@ -328,10 +424,17 @@ export function orderReadyQueue(items, options = {}) {
328
424
  ),
329
425
  humanNeededLabel: options.humanNeededLabel ?? DEFAULT_HUMAN_NEEDED_LABEL,
330
426
  };
331
- const judged = (items ?? []).map(item => ({
332
- item,
333
- reason: exclusionReason(item, context),
334
- }));
427
+ }
428
+
429
+ /**
430
+ * Split items into ordered eligible ones and excluded ones with reasons.
431
+ * @param {readonly Record<string, any>[] | undefined} items
432
+ * @param {(item: Record<string, any>) => string | null} judge - Exclusion reason, or null
433
+ * @param {ReturnType<typeof orderingContext>} context
434
+ * @returns {{ ordered: Record<string, any>[], excluded: { ref: string, reason: string }[] }}
435
+ */
436
+ function rank(items, judge, context) {
437
+ const judged = (items ?? []).map(item => ({ item, reason: judge(item) }));
335
438
  const eligible = judged
336
439
  .filter(one => one.reason === null)
337
440
  .map(one => one.item);
@@ -819,10 +819,38 @@ export function validateSessionManager(block) {
819
819
  ]
820
820
  : []),
821
821
  ...validateOperatorIds(block.operatorIds),
822
+ ...validateBacklogPickup(block.backlogPickup, block.operatorIds),
822
823
  ...findCredentials(block, "sessionManager"),
823
824
  ];
824
825
  }
825
826
 
827
+ /**
828
+ * Validate `sessionManager.backlogPickup`: opt-in promotion of validated
829
+ * backlog items to Ready, assigned to `operatorIds[0]`. Without an operator
830
+ * there is nobody to assign the promoted item to, so it is refused rather
831
+ * than promoting work into the agent's own name.
832
+ * @param {unknown} value The value, if present.
833
+ * @param {unknown} operatorIds `sessionManager.operatorIds`, if present.
834
+ * @returns {string[]} Problems found.
835
+ */
836
+ function validateBacklogPickup(value, operatorIds) {
837
+ if (value === undefined) return [];
838
+ if (typeof value !== "boolean") {
839
+ return [
840
+ `sessionManager.backlogPickup must be true or false (absent means false).`,
841
+ ];
842
+ }
843
+ const hasOperator =
844
+ Array.isArray(operatorIds) &&
845
+ operatorIds.some(id => typeof id === "string" && id.trim() !== "");
846
+ return value && !hasOperator
847
+ ? [
848
+ `sessionManager.backlogPickup needs sessionManager.operatorIds: a ` +
849
+ `promoted backlog item is assigned to the first operator.`,
850
+ ]
851
+ : [];
852
+ }
853
+
826
854
  /**
827
855
  * Validate every block this plan introduced.
828
856
  * @param {object} cfg Parsed `.lisa.config.json`.
@@ -214,6 +214,17 @@ Skip while `capacity` reports `paused`. Otherwise:
214
214
  createdAt, assignee, labels, description, creator and children with their state type.
215
215
  - `ordered` is the dispatch order: assigned to an operator, then to "me", then unassigned; each
216
216
  group by priority, then age. `excluded` needs no action.
217
+ - **Backlog tier** (only when `sessionManager.backlogPickup` is `true`). GitHub: `queue` fetches
218
+ it itself (open issues with no build or PRD lifecycle label). Jira / Linear: also fetch the
219
+ open leaf items in **no** lifecycle role — Linear workflow state type `backlog` or `unstarted`
220
+ other than the configured `ready` state; Jira status category "To Do" other than the configured
221
+ `ready` status — paged to completion, including each item's `updatedAt` / `updated` and its
222
+ blockers (Linear `inverseRelations` with `type` and the blocking issue's `state.type`; Jira
223
+ `issuelinks`). Put them in the same input file as
224
+ `"backlog":{"complete":true,"pages":<n>,"items":[...]}`; `queue` refuses a missing or partial
225
+ backlog envelope while the key is on. It returns `backlogOrdered` (same grouping and order,
226
+ minus items with an unfinished blocker and items whose cached validation failure still matches
227
+ their `updatedAt`) and `backlogExcluded`.
217
228
  2. **While `capacity` says `can_spawn`** and items remain, take the next item that has no pending
218
229
  question in the ledger:
219
230
  1. Read it fully (`lisa-tracker-read`): title, description, labels, project, parent, comments.
@@ -244,6 +255,34 @@ Skip while `capacity` reports `paused`. Otherwise:
244
255
  `recovery-required` with that message as the summary. It is not a failed attempt; do not
245
256
  dispatch to that repo again until the operator fixes it.
246
257
  - any other exit: move the item back to the `ready` role, notify Telegram, continue.
258
+ 3. **Backlog tier, lowest priority.** Only when `backlogPickup` is on, every `ordered` item has been
259
+ taken or skipped, and `capacity` still says `can_spawn`. Ready work always wins: never promote a
260
+ backlog item while a dispatchable Ready item remains. For the next `backlogOrdered` item with no
261
+ pending question in the ledger:
262
+ 1. Read it fully (`lisa-tracker-read`). If it gained an assignee who is neither "me" nor an
263
+ operator, a lifecycle role, open child work, a human hold or an unfinished blocker since the
264
+ queue was listed, skip it.
265
+ 2. **Validate it** with `lisa-tracker-validate` against the live item (identifier mode, all
266
+ gates, including the stateless-pickup dry-run). This is what "can be taken to completion"
267
+ means. On a definitive `FAIL`, record
268
+ `ledger validation-failed <ref> --updated-at <the item's updatedAt>` with a one-line summary
269
+ of the failing gates on STDIN, and skip it; the queue leaves it out until the item changes.
270
+ On could-not-validate (an outage or unreadable item, not a verdict), skip it WITHOUT
271
+ recording, so the next cycle retries it. Either way, post nothing to Telegram: backlog
272
+ grooming is not the operator's inbox.
273
+ 3. **Route the repo** as in step 2.2, but never ask: an ambiguous or multi-repo backlog item is
274
+ skipped.
275
+ 4. **Promote** in ONE `lisa-tracker-write` update: assignee = the first `operatorIds` entry, move
276
+ to the `ready` role, comment
277
+ `[lisa-session-manager] Promoted from backlog: validation passed; assigned to the operator.`
278
+ Read it back with `lisa-tracker-read` and continue only when the role is `ready` and the
279
+ assignee is that operator. Then `ledger validation-cleared <ref>`.
280
+ 5. Continue with steps 2.3 and 2.4 (claim in the child repo, re-read, spawn, handle the exit)
281
+ exactly as for a Ready item, with one change: on exit 0, post
282
+ `[<REF>] promoted from backlog and started in <repo> (<name>)` in place of the plain start
283
+ notice. Every other exit keeps its step 2.4 handling.
284
+ A promotion written and read back counts as a state change for the run outcome; a spawn that
285
+ then fails follows step 2.4's exits (the item stays in Ready, assigned to the operator).
247
286
 
248
287
  ### 3. Report and record
249
288
 
@@ -23,6 +23,12 @@ then to the manager's own tracker identity, then unassigned; within each, by pri
23
23
  Items assigned to someone else, containers, human-held items and (when configured) items by
24
24
  untrusted authors are skipped. As many as the machine has capacity for.
25
25
 
26
+ When `sessionManager.backlogPickup` is on and capacity remains after every Ready item, a lowest tier
27
+ follows: backlog items (open leaves in no lifecycle role) in the same order, minus those with an
28
+ unfinished blocker and those that already failed validation and have not changed since. Each one
29
+ must pass the full tracker validation before it is assigned to the first operator, moved to Ready
30
+ and dispatched like any other item.
31
+
26
32
  ## Scope/bounds
27
33
 
28
34
  Only the repos listed in `sessionManager.repos` (or this repo). It never writes code itself, never
@@ -24,9 +24,11 @@ import {
24
24
  import { parseJson, readJsonFile } from "./cli-queue.mjs";
25
25
  import {
26
26
  clearPendingQuestion,
27
+ clearValidationFailure,
27
28
  readLedger,
28
29
  recordFinish,
29
30
  recordSpawn,
31
+ recordValidationFailure,
30
32
  setControl,
31
33
  setPendingQuestion,
32
34
  } from "./ledger.mjs";
@@ -130,6 +132,14 @@ const LEDGER_ACTIONS = {
130
132
  reason: stdinText(ctx, "reason", { required: false }),
131
133
  });
132
134
  },
135
+ "validation-failed": ctx =>
136
+ recordValidationFailure(ctx.root, {
137
+ ref: requirePositional(ctx, "ref"),
138
+ updatedAt: requireValue(ctx, "updated-at"),
139
+ reason: stdinText(ctx, "reason", { required: false }),
140
+ }),
141
+ "validation-cleared": ctx =>
142
+ clearValidationFailure(ctx.root, { ref: requirePositional(ctx, "ref") }),
133
143
  control: ctx =>
134
144
  setControl(
135
145
  ctx.root,