@codyswann/lisa 4.83.2 → 4.84.1
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.
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +17 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa/scripts/ready-queue-order.mjs +109 -6
- package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/ready-queue-order.mjs +109 -6
- package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa-agy/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa-agy/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-copilot/scripts/ready-queue-order.mjs +109 -6
- package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa-copilot/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa-copilot/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +1 -0
- package/plugins/lisa-cursor/scripts/ready-queue-order.mjs +109 -6
- package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa-cursor/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa-cursor/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-devin/rules/reference/config-resolution.md +1 -0
- package/plugins/lisa-devin/scripts/ready-queue-order.mjs +109 -6
- package/plugins/lisa-devin/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/lisa-devin/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/lisa-devin/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
- package/plugins/src/base/rules/reference/config-resolution.md +1 -0
- package/plugins/src/base/scripts/ready-queue-order.mjs +109 -6
- package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +28 -0
- package/plugins/src/base/skills/lisa-session-manager/SKILL.md +39 -0
- package/plugins/src/base/skills/lisa-session-manager/references/runbook-template.md +6 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-cycle.mjs +10 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-queue.mjs +145 -18
- package/plugins/src/base/skills/lisa-session-manager/scripts/cli-usage.mjs +13 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/config.mjs +1 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/ledger.mjs +70 -0
- package/plugins/src/base/skills/lisa-session-manager/scripts/session-manager.mjs +2 -0
- package/plugins/src/base/skills/lisa-setup-session-manager/SKILL.md +8 -1
- package/typescript/copy-overwrite/scripts/check-skipped-required-checks.mjs +276 -6
|
@@ -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.
|
|
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
|
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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,
|