@codyswann/lisa 2.100.2 → 2.101.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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/scripts/doctor-report.mjs +14 -3
- package/plugins/lisa/scripts/queue-status-build-readers.mjs +39 -4
- package/plugins/lisa/scripts/queue-status-prd-readers.mjs +39 -4
- package/plugins/lisa/skills/intake-explain/SKILL.md +27 -0
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-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-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-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-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-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-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/src/base/scripts/doctor-report.mjs +14 -3
- package/plugins/src/base/scripts/queue-status-build-readers.mjs +39 -4
- package/plugins/src/base/scripts/queue-status-prd-readers.mjs +39 -4
- package/plugins/src/base/skills/intake-explain/SKILL.md +27 -0
package/package.json
CHANGED
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"lodash": ">=4.18.1"
|
|
83
83
|
},
|
|
84
84
|
"name": "@codyswann/lisa",
|
|
85
|
-
"version": "2.
|
|
85
|
+
"version": "2.101.1",
|
|
86
86
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
87
87
|
"main": "dist/index.js",
|
|
88
88
|
"exports": {
|
|
@@ -39,7 +39,7 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
|
39
39
|
* @returns {DoctorVerdict}
|
|
40
40
|
*/
|
|
41
41
|
export function computeDoctorVerdict(groups) {
|
|
42
|
-
const checks = groups.flatMap(group => group.checks);
|
|
42
|
+
const checks = groups.flatMap(group => group.checks.map(normalizeCheck));
|
|
43
43
|
if (checks.some(check => check.status === "FAIL")) {
|
|
44
44
|
return "NOT_READY";
|
|
45
45
|
}
|
|
@@ -55,7 +55,7 @@ export function computeDoctorVerdict(groups) {
|
|
|
55
55
|
*/
|
|
56
56
|
export function countDoctorStatuses(groups) {
|
|
57
57
|
return groups
|
|
58
|
-
.flatMap(group => group.checks)
|
|
58
|
+
.flatMap(group => group.checks.map(normalizeCheck))
|
|
59
59
|
.reduce(
|
|
60
60
|
(counts, check) => ({
|
|
61
61
|
...counts,
|
|
@@ -111,9 +111,20 @@ export function renderDoctorReport(input) {
|
|
|
111
111
|
* @returns {DoctorGroup}
|
|
112
112
|
*/
|
|
113
113
|
function normalizeGroup(group) {
|
|
114
|
+
const checks =
|
|
115
|
+
group.checks.length === 0
|
|
116
|
+
? [
|
|
117
|
+
{
|
|
118
|
+
id: "empty-group",
|
|
119
|
+
status: "SKIP",
|
|
120
|
+
summary: "no checks registered yet",
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: group.checks.map(normalizeCheck);
|
|
124
|
+
|
|
114
125
|
return {
|
|
115
126
|
...group,
|
|
116
|
-
checks
|
|
127
|
+
checks,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
|
|
@@ -19,6 +19,7 @@ export const BUILD_LIFECYCLE_ORDER = [
|
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
const ACTIONABLE_ROLE_ORDER = ["blocked", "ready", "claimed", "review"];
|
|
22
|
+
const RAW_BUILD_READER_TRACKERS = new Set(["github"]);
|
|
22
23
|
|
|
23
24
|
const HIGHLIGHT_COPY = {
|
|
24
25
|
blocked: {
|
|
@@ -89,6 +90,8 @@ export function readGithubBuildQueueSnapshot(input = {}) {
|
|
|
89
90
|
* }} input
|
|
90
91
|
*/
|
|
91
92
|
export function createBuildQueueSnapshot(input = {}) {
|
|
93
|
+
const tracker = normalizeTracker(input.tracker);
|
|
94
|
+
const unsupportedReaderError = resolveUnsupportedReaderError(input, tracker);
|
|
92
95
|
const roles = normalizeRoles(input.roles);
|
|
93
96
|
const items = normalizeItems(input.items);
|
|
94
97
|
const counts = buildLifecycleCounts(items);
|
|
@@ -99,9 +102,14 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
99
102
|
input.queueArgument
|
|
100
103
|
);
|
|
101
104
|
const queueResolved =
|
|
102
|
-
input.queueResolved ??
|
|
105
|
+
input.queueResolved ??
|
|
106
|
+
(unsupportedReaderError
|
|
107
|
+
? false
|
|
108
|
+
: typeof input.resolutionError !== "string");
|
|
103
109
|
const namespaceAdopted =
|
|
104
110
|
input.namespaceAdopted ?? inferNamespaceAdopted(items, roles);
|
|
111
|
+
const resolutionError =
|
|
112
|
+
unsupportedReaderError ?? input.resolutionError ?? null;
|
|
105
113
|
|
|
106
114
|
const health = classifyQueueHealth({
|
|
107
115
|
queueResolved,
|
|
@@ -110,11 +118,11 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
110
118
|
activeCount: counts.claimed + counts.review,
|
|
111
119
|
blockedCount: counts.blocked,
|
|
112
120
|
stalledCount: repairSignals.stalled.length,
|
|
113
|
-
resolutionError
|
|
121
|
+
resolutionError,
|
|
114
122
|
});
|
|
115
123
|
|
|
116
124
|
return {
|
|
117
|
-
tracker
|
|
125
|
+
tracker,
|
|
118
126
|
queueResolved,
|
|
119
127
|
namespaceAdopted,
|
|
120
128
|
roles,
|
|
@@ -122,7 +130,7 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
122
130
|
highlights,
|
|
123
131
|
repairSignals,
|
|
124
132
|
health,
|
|
125
|
-
resolutionError
|
|
133
|
+
resolutionError,
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -218,6 +226,33 @@ function normalizeRoles(roles) {
|
|
|
218
226
|
};
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {string | undefined} tracker
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
function normalizeTracker(tracker) {
|
|
234
|
+
return typeof tracker === "string" && tracker.trim().length > 0
|
|
235
|
+
? tracker.trim().toLowerCase()
|
|
236
|
+
: "unknown";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {Record<string, any>} input
|
|
241
|
+
* @param {string} tracker
|
|
242
|
+
* @returns {string | null}
|
|
243
|
+
*/
|
|
244
|
+
function resolveUnsupportedReaderError(input, tracker) {
|
|
245
|
+
if (
|
|
246
|
+
tracker === "unknown" ||
|
|
247
|
+
RAW_BUILD_READER_TRACKERS.has(tracker) ||
|
|
248
|
+
Object.hasOwn(input, "items")
|
|
249
|
+
) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return `vendor reader not implemented for build tracker '${tracker}'`;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
/**
|
|
222
257
|
* @param {Record<string, any> | undefined} done
|
|
223
258
|
* @returns {Record<string, string>}
|
|
@@ -27,6 +27,7 @@ const ACTIONABLE_ROLE_ORDER = [
|
|
|
27
27
|
"ready",
|
|
28
28
|
"ticketed",
|
|
29
29
|
];
|
|
30
|
+
const RAW_PRD_READER_SOURCES = new Set(["github"]);
|
|
30
31
|
|
|
31
32
|
const HIGHLIGHT_COPY = {
|
|
32
33
|
blocked: {
|
|
@@ -96,15 +97,22 @@ export function readGithubPrdQueueSnapshot(input = {}) {
|
|
|
96
97
|
* }} input
|
|
97
98
|
*/
|
|
98
99
|
export function createPrdQueueSnapshot(input = {}) {
|
|
100
|
+
const source = normalizeSource(input.source);
|
|
101
|
+
const unsupportedReaderError = resolveUnsupportedReaderError(input, source);
|
|
99
102
|
const rawRoles = input.roles ?? {};
|
|
100
103
|
const roles = normalizeRoles(rawRoles);
|
|
101
104
|
const items = normalizeItems(input.items);
|
|
102
105
|
const counts = buildLifecycleCounts(items);
|
|
103
106
|
const highlights = buildActionableHighlights(items, input.queueArgument);
|
|
104
107
|
const queueResolved =
|
|
105
|
-
input.queueResolved ??
|
|
108
|
+
input.queueResolved ??
|
|
109
|
+
(unsupportedReaderError
|
|
110
|
+
? false
|
|
111
|
+
: typeof input.resolutionError !== "string");
|
|
106
112
|
const namespaceAdopted =
|
|
107
113
|
input.namespaceAdopted ?? inferNamespaceAdopted(items, rawRoles);
|
|
114
|
+
const resolutionError =
|
|
115
|
+
unsupportedReaderError ?? input.resolutionError ?? null;
|
|
108
116
|
|
|
109
117
|
const health = classifyQueueHealth({
|
|
110
118
|
queueResolved,
|
|
@@ -113,18 +121,18 @@ export function createPrdQueueSnapshot(input = {}) {
|
|
|
113
121
|
activeCount: counts.in_review + counts.ticketed,
|
|
114
122
|
blockedCount: counts.blocked,
|
|
115
123
|
stalledCount: counts.shipped,
|
|
116
|
-
resolutionError
|
|
124
|
+
resolutionError,
|
|
117
125
|
});
|
|
118
126
|
|
|
119
127
|
return {
|
|
120
|
-
source
|
|
128
|
+
source,
|
|
121
129
|
queueResolved,
|
|
122
130
|
namespaceAdopted,
|
|
123
131
|
roles,
|
|
124
132
|
counts,
|
|
125
133
|
highlights,
|
|
126
134
|
health,
|
|
127
|
-
resolutionError
|
|
135
|
+
resolutionError,
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -203,6 +211,33 @@ function normalizeRoles(roles) {
|
|
|
203
211
|
return normalized;
|
|
204
212
|
}
|
|
205
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {string | undefined} source
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function normalizeSource(source) {
|
|
219
|
+
return typeof source === "string" && source.trim().length > 0
|
|
220
|
+
? source.trim().toLowerCase()
|
|
221
|
+
: "unknown";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {Record<string, any>} input
|
|
226
|
+
* @param {string} source
|
|
227
|
+
* @returns {string | null}
|
|
228
|
+
*/
|
|
229
|
+
function resolveUnsupportedReaderError(input, source) {
|
|
230
|
+
if (
|
|
231
|
+
source === "unknown" ||
|
|
232
|
+
RAW_PRD_READER_SOURCES.has(source) ||
|
|
233
|
+
Object.hasOwn(input, "items")
|
|
234
|
+
) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return `vendor reader not implemented for PRD source '${source}'`;
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
/**
|
|
207
242
|
* @param {readonly Record<string, any>[] | undefined} items
|
|
208
243
|
* @returns {readonly Record<string, any>[]}
|
|
@@ -143,6 +143,33 @@ The explanation must stay aligned with existing Lisa rules:
|
|
|
143
143
|
- If a claimed, in-review, or blocked item is not yet repairable, explain the relevant staleness or backoff condition at a human-readable level.
|
|
144
144
|
- If the source lane, tracker lane, repo/project scope, or lifecycle namespace is unresolved, report `MISCONFIGURED` instead of pretending the item is idle or actionable.
|
|
145
145
|
|
|
146
|
+
## PRD item role and repair diagnosis
|
|
147
|
+
|
|
148
|
+
For PRD lifecycle items, run the same read-side checks that PRD intake and repair-intake use before they would claim, validate, roll up, or retry a PRD. This is still a read-only explanation: if execution intake would transition `ready -> in_review`, route a `ticketed` PRD through rollup, move a `blocked` PRD after new answers, or suppress repair because a `[lisa-repair-intake]` marker is still inside its backoff window, intake-explain reports that decision and does not mutate the PRD, comments, labels, parent page, project labels, or generated work.
|
|
149
|
+
|
|
150
|
+
Resolve the source lane from `.lisa.config.json` `source` and the PRD lifecycle roles from the same vendor-specific config keys PRD intake and repair-intake use (`github.labels.prd.*`, `linear.labels.prd.*`, `notion.values.*`, or `confluence.parents.*`) with the usual defaults (`draft`, `ready`, `in_review`, `blocked`, `ticketed`, `shipped`, and `verified` equivalents). If the current item carries conflicting PRD lifecycle roles, lacks an adopted PRD namespace, or cannot be tied to the configured source lane, return `MISCONFIGURED` rather than guessing.
|
|
151
|
+
|
|
152
|
+
For GitHub-backed PRDs, collect these reader signals before choosing a verdict:
|
|
153
|
+
|
|
154
|
+
- current PRD lifecycle role label and any conflicting `prd-*` labels
|
|
155
|
+
- source-lane role ownership: `draft`, `shipped`, and `verified` are product-owned or verification-owned; `ready`, `in_review`, `blocked`, and `ticketed` are Lisa-owned for intake, repair, or rollup purposes
|
|
156
|
+
- provider-native timestamps (`updatedAt`) plus latest PRD comments, with special attention to comments after the most recent blocked or in-review marker
|
|
157
|
+
- `[lisa-repair-intake]` marker comments, including state fingerprint, repair verdict, retry count, and backoff window when present
|
|
158
|
+
- generated top-level work from native sub-issues first, then the `## Tickets` / `## Generated Work` section used by `prd-lifecycle-rollup`
|
|
159
|
+
- generated child terminal status so `ticketed` PRDs can explain whether rollup is waiting on downstream work or ready for `/lisa:repair-intake`
|
|
160
|
+
- clarifying-answer signals on `blocked` PRDs: new product comments, changed body text, changed blocker refs, or cleared dependencies compared with the last blocked/repair fingerprint
|
|
161
|
+
|
|
162
|
+
Apply PRD verdicts in the same order as PRD intake and repair-intake:
|
|
163
|
+
|
|
164
|
+
1. **Product-owned or verification-owned roles.** A PRD in `draft` returns `PRODUCT_OWNED_STATE`; the next action is manual product clarification or promotion to the configured `ready` role. A PRD in `shipped` returns `PRODUCT_OWNED_STATE` with `/lisa:verify-prd <item-ref>` as the next Lisa workflow because shipped-to-verified acceptance is outside PRD intake. A PRD in `verified` returns `PRODUCT_OWNED_STATE` or a terminal no-op explanation; normal intake and repair must not mutate it.
|
|
165
|
+
2. **Ready PRD.** A PRD in the configured `ready` role returns `ELIGIBLE_FOR_INTAKE` when the source lane and lifecycle namespace resolve cleanly. The `Why:` line should say PRD intake would claim it into the configured `in_review` role and run the source-to-tracker dry-run validate-to-route pipeline.
|
|
166
|
+
3. **In-review PRD.** A PRD in `in_review` is already Lisa-owned. Compare the newest activity signal against the resolved repair `stale_after` threshold (`$ARGUMENTS` override if present, `.lisa.config.json` `intake.repair.staleAfterHours`, then the 24h default). If provider activity, a progress comment, or a source edit is newer than `now - stale_after`, return `WAITING_ON_STALENESS` and name the activity timestamp. If no reliable timestamp exists, return `WAITING_ON_STALENESS` unless the caller explicitly uses the repair flow with `stale_after=0`; intake-explain should not imply automatic recovery from unknown freshness. If the item is stale and no repair-backoff marker suppresses retry, return `ELIGIBLE_FOR_REPAIR`.
|
|
167
|
+
4. **Blocked PRD.** A PRD in `blocked` is Lisa-owned but repairable only when the next validate-to-route pass can materially differ. Treat new human answers after the blocking comment, body edits after the blocking marker, cleared blockers, changed blocker refs, or changed validator fingerprint as repair-enabling signals. If none are present, return `HELD_BY_BLOCKERS` or `WAITING_ON_STALENESS` depending on whether the decisive fact is an active blocker/unchanged clarification need or a still-fresh blocked marker. If the blocker/answer state changed and repair backoff is clear, return `ELIGIBLE_FOR_REPAIR`.
|
|
168
|
+
5. **Ticketed PRD.** A PRD in `ticketed` is not ready for first intake. Read generated top-level work using the same `prd-lifecycle-rollup` boundary as PRD intake: top-level Epics and top-level Stories count; nested Stories and Sub-tasks do not. If any generated top-level child is non-terminal, return `PRODUCT_OWNED_STATE` or a waiting explanation that downstream build work is still in progress. If all generated top-level work is terminal but the PRD has not rolled up to `shipped`, return `ELIGIBLE_FOR_REPAIR` and recommend `/lisa:repair-intake <queue>` to reconcile rollup drift.
|
|
169
|
+
6. **Repair backoff suppression.** Before returning `ELIGIBLE_FOR_REPAIR` for `in_review`, `blocked`, or `ticketed` PRDs, inspect `[lisa-repair-intake]` markers. If the latest marker's state fingerprint matches the current reader signals and its backoff window has not expired, return `WAITING_ON_STALENESS` with a `Why:` line that says repair-intake would suppress an unchanged retry to avoid a loop. If the fingerprint changed, the backoff expired, or a human uses repair-intake `force=true`, the item may be repair-eligible.
|
|
170
|
+
|
|
171
|
+
Relevant `Signals:` should include the decisive context, not every field: for example `prd-blocked; new product comment after blocker`, `prd-in-review; last activity 2h ago; stale_after=24h`, `prd-ticketed; generated top-level work #12/#13 terminal`, or `[lisa-repair-intake] fingerprint unchanged; backoff until 2026-05-27T12:00:00Z`.
|
|
172
|
+
|
|
146
173
|
## Build item gate diagnosis
|
|
147
174
|
|
|
148
175
|
For build lifecycle items, run the same read-side checks that build intake runs before it would claim an issue. This is still a read-only explanation: if execution intake would stamp repo labels, split a cross-repo leaf, move a stale container from `ready` to `claimed`, or post a dependency-hold comment, intake-explain reports what intake would do but does not stamp, does not split, does not move labels, and does not comment.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.101.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.101.1",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -39,7 +39,7 @@ export const DOCTOR_VERDICTS = ["READY", "READY_WITH_WARNINGS", "NOT_READY"];
|
|
|
39
39
|
* @returns {DoctorVerdict}
|
|
40
40
|
*/
|
|
41
41
|
export function computeDoctorVerdict(groups) {
|
|
42
|
-
const checks = groups.flatMap(group => group.checks);
|
|
42
|
+
const checks = groups.flatMap(group => group.checks.map(normalizeCheck));
|
|
43
43
|
if (checks.some(check => check.status === "FAIL")) {
|
|
44
44
|
return "NOT_READY";
|
|
45
45
|
}
|
|
@@ -55,7 +55,7 @@ export function computeDoctorVerdict(groups) {
|
|
|
55
55
|
*/
|
|
56
56
|
export function countDoctorStatuses(groups) {
|
|
57
57
|
return groups
|
|
58
|
-
.flatMap(group => group.checks)
|
|
58
|
+
.flatMap(group => group.checks.map(normalizeCheck))
|
|
59
59
|
.reduce(
|
|
60
60
|
(counts, check) => ({
|
|
61
61
|
...counts,
|
|
@@ -111,9 +111,20 @@ export function renderDoctorReport(input) {
|
|
|
111
111
|
* @returns {DoctorGroup}
|
|
112
112
|
*/
|
|
113
113
|
function normalizeGroup(group) {
|
|
114
|
+
const checks =
|
|
115
|
+
group.checks.length === 0
|
|
116
|
+
? [
|
|
117
|
+
{
|
|
118
|
+
id: "empty-group",
|
|
119
|
+
status: "SKIP",
|
|
120
|
+
summary: "no checks registered yet",
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
: group.checks.map(normalizeCheck);
|
|
124
|
+
|
|
114
125
|
return {
|
|
115
126
|
...group,
|
|
116
|
-
checks
|
|
127
|
+
checks,
|
|
117
128
|
};
|
|
118
129
|
}
|
|
119
130
|
|
|
@@ -19,6 +19,7 @@ export const BUILD_LIFECYCLE_ORDER = [
|
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
const ACTIONABLE_ROLE_ORDER = ["blocked", "ready", "claimed", "review"];
|
|
22
|
+
const RAW_BUILD_READER_TRACKERS = new Set(["github"]);
|
|
22
23
|
|
|
23
24
|
const HIGHLIGHT_COPY = {
|
|
24
25
|
blocked: {
|
|
@@ -89,6 +90,8 @@ export function readGithubBuildQueueSnapshot(input = {}) {
|
|
|
89
90
|
* }} input
|
|
90
91
|
*/
|
|
91
92
|
export function createBuildQueueSnapshot(input = {}) {
|
|
93
|
+
const tracker = normalizeTracker(input.tracker);
|
|
94
|
+
const unsupportedReaderError = resolveUnsupportedReaderError(input, tracker);
|
|
92
95
|
const roles = normalizeRoles(input.roles);
|
|
93
96
|
const items = normalizeItems(input.items);
|
|
94
97
|
const counts = buildLifecycleCounts(items);
|
|
@@ -99,9 +102,14 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
99
102
|
input.queueArgument
|
|
100
103
|
);
|
|
101
104
|
const queueResolved =
|
|
102
|
-
input.queueResolved ??
|
|
105
|
+
input.queueResolved ??
|
|
106
|
+
(unsupportedReaderError
|
|
107
|
+
? false
|
|
108
|
+
: typeof input.resolutionError !== "string");
|
|
103
109
|
const namespaceAdopted =
|
|
104
110
|
input.namespaceAdopted ?? inferNamespaceAdopted(items, roles);
|
|
111
|
+
const resolutionError =
|
|
112
|
+
unsupportedReaderError ?? input.resolutionError ?? null;
|
|
105
113
|
|
|
106
114
|
const health = classifyQueueHealth({
|
|
107
115
|
queueResolved,
|
|
@@ -110,11 +118,11 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
110
118
|
activeCount: counts.claimed + counts.review,
|
|
111
119
|
blockedCount: counts.blocked,
|
|
112
120
|
stalledCount: repairSignals.stalled.length,
|
|
113
|
-
resolutionError
|
|
121
|
+
resolutionError,
|
|
114
122
|
});
|
|
115
123
|
|
|
116
124
|
return {
|
|
117
|
-
tracker
|
|
125
|
+
tracker,
|
|
118
126
|
queueResolved,
|
|
119
127
|
namespaceAdopted,
|
|
120
128
|
roles,
|
|
@@ -122,7 +130,7 @@ export function createBuildQueueSnapshot(input = {}) {
|
|
|
122
130
|
highlights,
|
|
123
131
|
repairSignals,
|
|
124
132
|
health,
|
|
125
|
-
resolutionError
|
|
133
|
+
resolutionError,
|
|
126
134
|
};
|
|
127
135
|
}
|
|
128
136
|
|
|
@@ -218,6 +226,33 @@ function normalizeRoles(roles) {
|
|
|
218
226
|
};
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {string | undefined} tracker
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
function normalizeTracker(tracker) {
|
|
234
|
+
return typeof tracker === "string" && tracker.trim().length > 0
|
|
235
|
+
? tracker.trim().toLowerCase()
|
|
236
|
+
: "unknown";
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {Record<string, any>} input
|
|
241
|
+
* @param {string} tracker
|
|
242
|
+
* @returns {string | null}
|
|
243
|
+
*/
|
|
244
|
+
function resolveUnsupportedReaderError(input, tracker) {
|
|
245
|
+
if (
|
|
246
|
+
tracker === "unknown" ||
|
|
247
|
+
RAW_BUILD_READER_TRACKERS.has(tracker) ||
|
|
248
|
+
Object.hasOwn(input, "items")
|
|
249
|
+
) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return `vendor reader not implemented for build tracker '${tracker}'`;
|
|
254
|
+
}
|
|
255
|
+
|
|
221
256
|
/**
|
|
222
257
|
* @param {Record<string, any> | undefined} done
|
|
223
258
|
* @returns {Record<string, string>}
|
|
@@ -27,6 +27,7 @@ const ACTIONABLE_ROLE_ORDER = [
|
|
|
27
27
|
"ready",
|
|
28
28
|
"ticketed",
|
|
29
29
|
];
|
|
30
|
+
const RAW_PRD_READER_SOURCES = new Set(["github"]);
|
|
30
31
|
|
|
31
32
|
const HIGHLIGHT_COPY = {
|
|
32
33
|
blocked: {
|
|
@@ -96,15 +97,22 @@ export function readGithubPrdQueueSnapshot(input = {}) {
|
|
|
96
97
|
* }} input
|
|
97
98
|
*/
|
|
98
99
|
export function createPrdQueueSnapshot(input = {}) {
|
|
100
|
+
const source = normalizeSource(input.source);
|
|
101
|
+
const unsupportedReaderError = resolveUnsupportedReaderError(input, source);
|
|
99
102
|
const rawRoles = input.roles ?? {};
|
|
100
103
|
const roles = normalizeRoles(rawRoles);
|
|
101
104
|
const items = normalizeItems(input.items);
|
|
102
105
|
const counts = buildLifecycleCounts(items);
|
|
103
106
|
const highlights = buildActionableHighlights(items, input.queueArgument);
|
|
104
107
|
const queueResolved =
|
|
105
|
-
input.queueResolved ??
|
|
108
|
+
input.queueResolved ??
|
|
109
|
+
(unsupportedReaderError
|
|
110
|
+
? false
|
|
111
|
+
: typeof input.resolutionError !== "string");
|
|
106
112
|
const namespaceAdopted =
|
|
107
113
|
input.namespaceAdopted ?? inferNamespaceAdopted(items, rawRoles);
|
|
114
|
+
const resolutionError =
|
|
115
|
+
unsupportedReaderError ?? input.resolutionError ?? null;
|
|
108
116
|
|
|
109
117
|
const health = classifyQueueHealth({
|
|
110
118
|
queueResolved,
|
|
@@ -113,18 +121,18 @@ export function createPrdQueueSnapshot(input = {}) {
|
|
|
113
121
|
activeCount: counts.in_review + counts.ticketed,
|
|
114
122
|
blockedCount: counts.blocked,
|
|
115
123
|
stalledCount: counts.shipped,
|
|
116
|
-
resolutionError
|
|
124
|
+
resolutionError,
|
|
117
125
|
});
|
|
118
126
|
|
|
119
127
|
return {
|
|
120
|
-
source
|
|
128
|
+
source,
|
|
121
129
|
queueResolved,
|
|
122
130
|
namespaceAdopted,
|
|
123
131
|
roles,
|
|
124
132
|
counts,
|
|
125
133
|
highlights,
|
|
126
134
|
health,
|
|
127
|
-
resolutionError
|
|
135
|
+
resolutionError,
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -203,6 +211,33 @@ function normalizeRoles(roles) {
|
|
|
203
211
|
return normalized;
|
|
204
212
|
}
|
|
205
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {string | undefined} source
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function normalizeSource(source) {
|
|
219
|
+
return typeof source === "string" && source.trim().length > 0
|
|
220
|
+
? source.trim().toLowerCase()
|
|
221
|
+
: "unknown";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {Record<string, any>} input
|
|
226
|
+
* @param {string} source
|
|
227
|
+
* @returns {string | null}
|
|
228
|
+
*/
|
|
229
|
+
function resolveUnsupportedReaderError(input, source) {
|
|
230
|
+
if (
|
|
231
|
+
source === "unknown" ||
|
|
232
|
+
RAW_PRD_READER_SOURCES.has(source) ||
|
|
233
|
+
Object.hasOwn(input, "items")
|
|
234
|
+
) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return `vendor reader not implemented for PRD source '${source}'`;
|
|
239
|
+
}
|
|
240
|
+
|
|
206
241
|
/**
|
|
207
242
|
* @param {readonly Record<string, any>[] | undefined} items
|
|
208
243
|
* @returns {readonly Record<string, any>[]}
|
|
@@ -143,6 +143,33 @@ The explanation must stay aligned with existing Lisa rules:
|
|
|
143
143
|
- If a claimed, in-review, or blocked item is not yet repairable, explain the relevant staleness or backoff condition at a human-readable level.
|
|
144
144
|
- If the source lane, tracker lane, repo/project scope, or lifecycle namespace is unresolved, report `MISCONFIGURED` instead of pretending the item is idle or actionable.
|
|
145
145
|
|
|
146
|
+
## PRD item role and repair diagnosis
|
|
147
|
+
|
|
148
|
+
For PRD lifecycle items, run the same read-side checks that PRD intake and repair-intake use before they would claim, validate, roll up, or retry a PRD. This is still a read-only explanation: if execution intake would transition `ready -> in_review`, route a `ticketed` PRD through rollup, move a `blocked` PRD after new answers, or suppress repair because a `[lisa-repair-intake]` marker is still inside its backoff window, intake-explain reports that decision and does not mutate the PRD, comments, labels, parent page, project labels, or generated work.
|
|
149
|
+
|
|
150
|
+
Resolve the source lane from `.lisa.config.json` `source` and the PRD lifecycle roles from the same vendor-specific config keys PRD intake and repair-intake use (`github.labels.prd.*`, `linear.labels.prd.*`, `notion.values.*`, or `confluence.parents.*`) with the usual defaults (`draft`, `ready`, `in_review`, `blocked`, `ticketed`, `shipped`, and `verified` equivalents). If the current item carries conflicting PRD lifecycle roles, lacks an adopted PRD namespace, or cannot be tied to the configured source lane, return `MISCONFIGURED` rather than guessing.
|
|
151
|
+
|
|
152
|
+
For GitHub-backed PRDs, collect these reader signals before choosing a verdict:
|
|
153
|
+
|
|
154
|
+
- current PRD lifecycle role label and any conflicting `prd-*` labels
|
|
155
|
+
- source-lane role ownership: `draft`, `shipped`, and `verified` are product-owned or verification-owned; `ready`, `in_review`, `blocked`, and `ticketed` are Lisa-owned for intake, repair, or rollup purposes
|
|
156
|
+
- provider-native timestamps (`updatedAt`) plus latest PRD comments, with special attention to comments after the most recent blocked or in-review marker
|
|
157
|
+
- `[lisa-repair-intake]` marker comments, including state fingerprint, repair verdict, retry count, and backoff window when present
|
|
158
|
+
- generated top-level work from native sub-issues first, then the `## Tickets` / `## Generated Work` section used by `prd-lifecycle-rollup`
|
|
159
|
+
- generated child terminal status so `ticketed` PRDs can explain whether rollup is waiting on downstream work or ready for `/lisa:repair-intake`
|
|
160
|
+
- clarifying-answer signals on `blocked` PRDs: new product comments, changed body text, changed blocker refs, or cleared dependencies compared with the last blocked/repair fingerprint
|
|
161
|
+
|
|
162
|
+
Apply PRD verdicts in the same order as PRD intake and repair-intake:
|
|
163
|
+
|
|
164
|
+
1. **Product-owned or verification-owned roles.** A PRD in `draft` returns `PRODUCT_OWNED_STATE`; the next action is manual product clarification or promotion to the configured `ready` role. A PRD in `shipped` returns `PRODUCT_OWNED_STATE` with `/lisa:verify-prd <item-ref>` as the next Lisa workflow because shipped-to-verified acceptance is outside PRD intake. A PRD in `verified` returns `PRODUCT_OWNED_STATE` or a terminal no-op explanation; normal intake and repair must not mutate it.
|
|
165
|
+
2. **Ready PRD.** A PRD in the configured `ready` role returns `ELIGIBLE_FOR_INTAKE` when the source lane and lifecycle namespace resolve cleanly. The `Why:` line should say PRD intake would claim it into the configured `in_review` role and run the source-to-tracker dry-run validate-to-route pipeline.
|
|
166
|
+
3. **In-review PRD.** A PRD in `in_review` is already Lisa-owned. Compare the newest activity signal against the resolved repair `stale_after` threshold (`$ARGUMENTS` override if present, `.lisa.config.json` `intake.repair.staleAfterHours`, then the 24h default). If provider activity, a progress comment, or a source edit is newer than `now - stale_after`, return `WAITING_ON_STALENESS` and name the activity timestamp. If no reliable timestamp exists, return `WAITING_ON_STALENESS` unless the caller explicitly uses the repair flow with `stale_after=0`; intake-explain should not imply automatic recovery from unknown freshness. If the item is stale and no repair-backoff marker suppresses retry, return `ELIGIBLE_FOR_REPAIR`.
|
|
167
|
+
4. **Blocked PRD.** A PRD in `blocked` is Lisa-owned but repairable only when the next validate-to-route pass can materially differ. Treat new human answers after the blocking comment, body edits after the blocking marker, cleared blockers, changed blocker refs, or changed validator fingerprint as repair-enabling signals. If none are present, return `HELD_BY_BLOCKERS` or `WAITING_ON_STALENESS` depending on whether the decisive fact is an active blocker/unchanged clarification need or a still-fresh blocked marker. If the blocker/answer state changed and repair backoff is clear, return `ELIGIBLE_FOR_REPAIR`.
|
|
168
|
+
5. **Ticketed PRD.** A PRD in `ticketed` is not ready for first intake. Read generated top-level work using the same `prd-lifecycle-rollup` boundary as PRD intake: top-level Epics and top-level Stories count; nested Stories and Sub-tasks do not. If any generated top-level child is non-terminal, return `PRODUCT_OWNED_STATE` or a waiting explanation that downstream build work is still in progress. If all generated top-level work is terminal but the PRD has not rolled up to `shipped`, return `ELIGIBLE_FOR_REPAIR` and recommend `/lisa:repair-intake <queue>` to reconcile rollup drift.
|
|
169
|
+
6. **Repair backoff suppression.** Before returning `ELIGIBLE_FOR_REPAIR` for `in_review`, `blocked`, or `ticketed` PRDs, inspect `[lisa-repair-intake]` markers. If the latest marker's state fingerprint matches the current reader signals and its backoff window has not expired, return `WAITING_ON_STALENESS` with a `Why:` line that says repair-intake would suppress an unchanged retry to avoid a loop. If the fingerprint changed, the backoff expired, or a human uses repair-intake `force=true`, the item may be repair-eligible.
|
|
170
|
+
|
|
171
|
+
Relevant `Signals:` should include the decisive context, not every field: for example `prd-blocked; new product comment after blocker`, `prd-in-review; last activity 2h ago; stale_after=24h`, `prd-ticketed; generated top-level work #12/#13 terminal`, or `[lisa-repair-intake] fingerprint unchanged; backoff until 2026-05-27T12:00:00Z`.
|
|
172
|
+
|
|
146
173
|
## Build item gate diagnosis
|
|
147
174
|
|
|
148
175
|
For build lifecycle items, run the same read-side checks that build intake runs before it would claim an issue. This is still a read-only explanation: if execution intake would stamp repo labels, split a cross-repo leaf, move a stale container from `ready` to `claimed`, or post a dependency-hold comment, intake-explain reports what intake would do but does not stamp, does not split, does not move labels, and does not comment.
|