@codyswann/lisa 3.33.8 → 3.35.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.
- package/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +399 -0
- package/dist/cli/doctor-traceability-gate.d.ts +39 -0
- package/dist/cli/doctor-traceability-gate.d.ts.map +1 -0
- package/dist/cli/doctor-traceability-gate.js +164 -0
- package/dist/cli/doctor-traceability-gate.js.map +1 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +7 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +3 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +32 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- 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/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/lisa/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/lisa/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-agy/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-agy/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- 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-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/lisa-copilot/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-copilot/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-copilot/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +19 -0
- package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +2 -0
- package/plugins/lisa-cursor/scripts/automation-run-record.mjs +146 -33
- package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/lisa-cursor/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/lisa-cursor/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- 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-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-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-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-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-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-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-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/src/base/rules/eager/automation-runbook-contract.md +2 -0
- package/plugins/src/base/rules/reference/automation-runbook-contract.md +19 -0
- package/plugins/src/base/scripts/automation-run-record.mjs +146 -33
- package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +190 -0
- package/plugins/src/base/scripts/intake-prework-denominator.mjs +249 -0
- package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +53 -3
- package/plugins/src/base/skills/lisa-intake/SKILL.md +17 -1
- package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +53 -5
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +48 -7
- package/plugins/src/base/skills/lisa-tracker-build-intake/SKILL.md +24 -0
- package/typescript/copy-contents/.husky/pre-commit +64 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pre-work denominator for build-queue intake scanners.
|
|
4
|
+
*
|
|
5
|
+
* A build-intake cycle that reports an empty lane is only as trustworthy as the
|
|
6
|
+
* set of lanes it swept. Sweeping by lane NAME (`Backlog`, `Todo`, `Ready`)
|
|
7
|
+
* omits every pre-work lane a team invented — most commonly a `Blocked` lane,
|
|
8
|
+
* which is not a distinct Linear state type at all: teams model it as
|
|
9
|
+
* `unstarted`, i.e. work that was never started. Items parked there carry a
|
|
10
|
+
* written blocker that nothing ever re-reads, so the lane reads as terminal
|
|
11
|
+
* while it is in fact a queue.
|
|
12
|
+
*
|
|
13
|
+
* This module fixes both halves: lanes are selected by TYPE, never by name, and
|
|
14
|
+
* the swept set is reported alongside the total so a wrong denominator is
|
|
15
|
+
* visible on inspection instead of silent forever.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Lane types that hold work which has not started. Both belong to intake. */
|
|
19
|
+
export const PRE_WORK_LANE_TYPES = Object.freeze(["backlog", "unstarted"]);
|
|
20
|
+
|
|
21
|
+
/** Loop ids whose `nothing-needed` runs must state their denominator. */
|
|
22
|
+
export const DENOMINATOR_REQUIRED_LOOP_IDS = Object.freeze(["intake-tickets"]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Vendor lane-type vocabularies normalized onto Linear's five-value type set.
|
|
26
|
+
* JIRA exposes the same idea as `statusCategory` (key or display name); GitHub
|
|
27
|
+
* has no type field at all, so its callers pass a type explicitly.
|
|
28
|
+
*/
|
|
29
|
+
const LANE_TYPE_ALIASES = Object.freeze({
|
|
30
|
+
backlog: "backlog",
|
|
31
|
+
unstarted: "unstarted",
|
|
32
|
+
started: "started",
|
|
33
|
+
completed: "completed",
|
|
34
|
+
canceled: "canceled",
|
|
35
|
+
cancelled: "canceled",
|
|
36
|
+
new: "unstarted",
|
|
37
|
+
indeterminate: "started",
|
|
38
|
+
done: "completed",
|
|
39
|
+
"to do": "unstarted",
|
|
40
|
+
"in progress": "started",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map a vendor lane type onto the normalized vocabulary.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} type
|
|
47
|
+
* @returns {string | null} normalized type, or null when unrecognized
|
|
48
|
+
*/
|
|
49
|
+
export function normalizeLaneType(type) {
|
|
50
|
+
if (typeof type !== "string") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return LANE_TYPE_ALIASES[type.trim().toLowerCase()] ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {unknown} type
|
|
58
|
+
* @returns {boolean} whether this lane holds not-yet-started work
|
|
59
|
+
*/
|
|
60
|
+
export function isPreWorkLaneType(type) {
|
|
61
|
+
const normalized = normalizeLaneType(type);
|
|
62
|
+
return normalized !== null && PRE_WORK_LANE_TYPES.includes(normalized);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {unknown} lane
|
|
67
|
+
* @returns {{ name: string, type: string | null, count: number, position: number } | null}
|
|
68
|
+
*/
|
|
69
|
+
function normalizeLane(lane) {
|
|
70
|
+
if (!lane || typeof lane !== "object") {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const name = String(lane.name ?? "").trim();
|
|
74
|
+
if (name.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const rawCount = Number(lane.count ?? 0);
|
|
78
|
+
const rawPosition = Number(lane.position ?? Number.POSITIVE_INFINITY);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
type: normalizeLaneType(lane.type ?? lane.statusCategory),
|
|
83
|
+
count: Number.isFinite(rawCount) && rawCount > 0 ? Math.trunc(rawCount) : 0,
|
|
84
|
+
position: Number.isFinite(rawPosition)
|
|
85
|
+
? rawPosition
|
|
86
|
+
: Number.POSITIVE_INFINITY,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {{ position: number, name: string }} left
|
|
92
|
+
* @param {{ position: number, name: string }} right
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
function compareLanes(left, right) {
|
|
96
|
+
if (left.position !== right.position) {
|
|
97
|
+
return left.position - right.position;
|
|
98
|
+
}
|
|
99
|
+
return left.name.localeCompare(right.name);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Select every lane holding not-yet-started work, by type.
|
|
104
|
+
*
|
|
105
|
+
* Deliberately name-blind: a team that adds `Blocked`, `Triaged`, or `Icebox`
|
|
106
|
+
* as an `unstarted` state gets it swept without a code change, and no hardcoded
|
|
107
|
+
* roster can fall behind the board.
|
|
108
|
+
*
|
|
109
|
+
* @param {readonly unknown[]} lanes
|
|
110
|
+
* @returns {readonly {name: string, type: string, count: number, position: number}[]}
|
|
111
|
+
*/
|
|
112
|
+
export function selectPreWorkLanes(lanes) {
|
|
113
|
+
return (Array.isArray(lanes) ? lanes : [])
|
|
114
|
+
.map(normalizeLane)
|
|
115
|
+
.filter(lane => lane !== null && isPreWorkLaneType(lane.type))
|
|
116
|
+
.sort(compareLanes);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build the denominator a scanner must report with its verdict.
|
|
121
|
+
*
|
|
122
|
+
* `totalOpen` is the count of open items on the queue, independent of lanes —
|
|
123
|
+
* supplying it is what makes an omitted lane arithmetically visible.
|
|
124
|
+
*
|
|
125
|
+
* @param {{ lanes?: readonly unknown[], totalOpen?: number }} input
|
|
126
|
+
* @returns {{
|
|
127
|
+
* swept: readonly {name: string, type: string, count: number}[]
|
|
128
|
+
* omitted: readonly {name: string, type: string | null, count: number}[]
|
|
129
|
+
* sweptCount: number
|
|
130
|
+
* totalOpen: number
|
|
131
|
+
* unsweptCount: number
|
|
132
|
+
* }}
|
|
133
|
+
*/
|
|
134
|
+
export function buildIntakeDenominator(input = {}) {
|
|
135
|
+
const normalized = (Array.isArray(input.lanes) ? input.lanes : [])
|
|
136
|
+
.map(normalizeLane)
|
|
137
|
+
.filter(lane => lane !== null)
|
|
138
|
+
.sort(compareLanes);
|
|
139
|
+
const swept = normalized.filter(lane => isPreWorkLaneType(lane.type));
|
|
140
|
+
const omitted = normalized.filter(lane => !isPreWorkLaneType(lane.type));
|
|
141
|
+
const sweptCount = swept.reduce((total, lane) => total + lane.count, 0);
|
|
142
|
+
const laneTotal = normalized.reduce((total, lane) => total + lane.count, 0);
|
|
143
|
+
const rawTotalOpen = Number(input.totalOpen);
|
|
144
|
+
const totalOpen =
|
|
145
|
+
Number.isFinite(rawTotalOpen) && rawTotalOpen >= 0
|
|
146
|
+
? Math.trunc(rawTotalOpen)
|
|
147
|
+
: laneTotal;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
swept: swept.map(({ name, type, count }) => ({ name, type, count })),
|
|
151
|
+
omitted: omitted.map(({ name, type, count }) => ({ name, type, count })),
|
|
152
|
+
sweptCount,
|
|
153
|
+
totalOpen,
|
|
154
|
+
unsweptCount: Math.max(totalOpen - sweptCount, 0),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render the denominator for a non-technical operator.
|
|
160
|
+
*
|
|
161
|
+
* @param {ReturnType<typeof buildIntakeDenominator>} denominator
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function formatIntakeDenominator(denominator) {
|
|
165
|
+
const lanes = denominator?.swept ?? [];
|
|
166
|
+
const listed =
|
|
167
|
+
lanes.length > 0
|
|
168
|
+
? lanes.map(lane => `${lane.name} (${lane.count})`).join(", ")
|
|
169
|
+
: "no pre-work lanes found";
|
|
170
|
+
|
|
171
|
+
return `Looked in every lane holding work that has not started — ${listed} — ${denominator.sweptCount} items checked out of ${denominator.totalOpen} still open.`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Render the full operator-readable one-liner for a dry build lane.
|
|
176
|
+
*
|
|
177
|
+
* @param {ReturnType<typeof buildIntakeDenominator>} denominator
|
|
178
|
+
* @param {{ queue?: string }} [context]
|
|
179
|
+
* @returns {string}
|
|
180
|
+
*/
|
|
181
|
+
export function summarizeDryLane(denominator, context = {}) {
|
|
182
|
+
const queue =
|
|
183
|
+
typeof context.queue === "string" && context.queue.trim().length > 0
|
|
184
|
+
? ` on ${context.queue.trim()}`
|
|
185
|
+
: "";
|
|
186
|
+
|
|
187
|
+
return `Nothing ready to build${queue}. ${formatIntakeDenominator(denominator)}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {unknown} loopId
|
|
192
|
+
* @param {unknown} outcome
|
|
193
|
+
* @returns {boolean} whether this run must carry a denominator
|
|
194
|
+
*/
|
|
195
|
+
export function requiresDenominator(loopId, outcome) {
|
|
196
|
+
return (
|
|
197
|
+
outcome === "nothing-needed" &&
|
|
198
|
+
DENOMINATOR_REQUIRED_LOOP_IDS.includes(String(loopId ?? "").trim())
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {unknown} denominator
|
|
204
|
+
* @returns {string | null} the reason it is unusable, or null when valid
|
|
205
|
+
*/
|
|
206
|
+
function findDenominatorDefect(denominator) {
|
|
207
|
+
if (!denominator || typeof denominator !== "object") {
|
|
208
|
+
return "no denominator was supplied";
|
|
209
|
+
}
|
|
210
|
+
if (!Array.isArray(denominator.swept) || denominator.swept.length === 0) {
|
|
211
|
+
return "it names no swept lanes";
|
|
212
|
+
}
|
|
213
|
+
if (!Number.isInteger(denominator.sweptCount)) {
|
|
214
|
+
return "its swept count is not a whole number";
|
|
215
|
+
}
|
|
216
|
+
if (!Number.isInteger(denominator.totalOpen)) {
|
|
217
|
+
return "its open-item total is not a whole number";
|
|
218
|
+
}
|
|
219
|
+
if (denominator.sweptCount > denominator.totalOpen) {
|
|
220
|
+
return "it claims to have swept more items than are open";
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Refuse to record a dry build lane that does not say what it looked at.
|
|
227
|
+
*
|
|
228
|
+
* Thirty-one consecutive cycles reported an empty lane against a queue holding
|
|
229
|
+
* unswept pre-work rows. Every record was honest and every conclusion was
|
|
230
|
+
* false, because none of them stated a denominator. This turns that silent
|
|
231
|
+
* wrong answer into a loud one.
|
|
232
|
+
*
|
|
233
|
+
* @param {{ loopId?: unknown, outcome?: unknown, denominator?: unknown }} input
|
|
234
|
+
* @throws {Error} when a denominator is required and missing or malformed
|
|
235
|
+
*/
|
|
236
|
+
export function assertDenominatorReported(input = {}) {
|
|
237
|
+
if (!requiresDenominator(input.loopId, input.outcome)) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const defect = findDenominatorDefect(input.denominator);
|
|
241
|
+
if (defect === null) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
throw new Error(
|
|
246
|
+
`A "nothing-needed" run for loop "${String(input.loopId).trim()}" must state the lanes it swept, and ${defect}. ` +
|
|
247
|
+
`Pass a denominator built by buildIntakeDenominator() so an omitted lane is visible instead of silent.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
@@ -110,7 +110,7 @@ The only legitimate reasons to stop early:
|
|
|
110
110
|
|
|
111
111
|
- Missing repo or required configuration. Surface the missing value and exit.
|
|
112
112
|
- Label namespace not adopted (no issue carries any of `$READY` / `$CLAIMED` / `$DONE`). Surface a label-convention error and exit (this is setup, not a normal idle cycle — see "Adoption" at the bottom).
|
|
113
|
-
- Empty
|
|
113
|
+
- Empty pre-work set. Exit cleanly on the denominator-stated summary from `summarizeDryLane` — which names every lane swept, its count, and the open total. A bare "nothing to do" is not an acceptable exit: it is indistinguishable from a wrong denominator (#2657).
|
|
114
114
|
|
|
115
115
|
## Lifecycle assumed
|
|
116
116
|
|
|
@@ -166,7 +166,57 @@ gh label list --repo <org>/<repo> --json name \
|
|
|
166
166
|
'[.[] | .name | select(. == $r or . == $c or (. as $n | $d | index($n)))] | length'
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
If none of the configured role labels exist on the repo → label convention not adopted, surface a setup error and exit.
|
|
169
|
+
If none of the configured role labels exist on the repo → label convention not adopted, surface a setup error and exit.
|
|
170
|
+
|
|
171
|
+
#### 2a. Sweep every pre-work lane, and state the denominator
|
|
172
|
+
|
|
173
|
+
GitHub Issues has no state-type field, so labels are the only lane available here — a constraint of
|
|
174
|
+
GitHub's data model, not a preference (see "Why labels" above). That makes the omission risk
|
|
175
|
+
*higher*, not lower: there is no `type` to fall back on, so the pre-work set must be derived from
|
|
176
|
+
**configured roles**, never from a hardcoded roster of label names. The pre-work lanes are:
|
|
177
|
+
|
|
178
|
+
- the configured `$READY` label — the human-flipped lane, worked first;
|
|
179
|
+
- the configured `$BLOCKED` label (`github.labels.build.blocked`) — items that were *never started*,
|
|
180
|
+
each carrying a written blocker that nothing re-read until Phase 2.5;
|
|
181
|
+
- open issues carrying **no** build role label — which this scanner must see anyway in order to
|
|
182
|
+
determine and stamp their repo.
|
|
183
|
+
|
|
184
|
+
Count each lane and the repo's **total open** count (`gh issue list --state open --limit 1000 --json
|
|
185
|
+
number | jq length`), then build the denominator with the shared helper — GitHub callers pass the
|
|
186
|
+
lane type explicitly, since there is none to read:
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
buildIntakeDenominator({ lanes: [{name: "$READY", type: "unstarted", count: <n>},
|
|
190
|
+
{name: "$BLOCKED", type: "unstarted", count: <n>},
|
|
191
|
+
{name: "(no role label)", type: "backlog", count: <n>},
|
|
192
|
+
{name: "$CLAIMED", type: "started", count: <n>}, …],
|
|
193
|
+
totalOpen: <open issue count> })
|
|
194
|
+
summarizeDryLane(denominator, { queue: "<org>/<repo>" })
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Every candidate outside `$READY` must clear Phase 2.5 before it is treated as a candidate at all. If
|
|
198
|
+
nothing survives, exit on the denominator-stated summary from `summarizeDryLane` — never a bare
|
|
199
|
+
"nothing to do". The run recorder rejects a dry build-intake run that does not name what it swept
|
|
200
|
+
(see `automation-runbook-contract`).
|
|
201
|
+
|
|
202
|
+
#### 2a.1 Re-probe the blockers instead of inheriting them
|
|
203
|
+
|
|
204
|
+
A blocker is a **claim with a timestamp, not a fact** — it goes stale the moment its condition comes
|
|
205
|
+
true, and nothing re-read one before this phase. For each `$BLOCKED` candidate:
|
|
206
|
+
|
|
207
|
+
1. **Human gate first, and it is absolute.** An issue carrying the configured human-needed label
|
|
208
|
+
(`github.labels.build.human_needed`, default `human-needed`) or a `[lisa-human-gate]` marker in
|
|
209
|
+
its body is **never** auto-selected, whatever any probe says.
|
|
210
|
+
2. **Extract the stated discharge condition**, then **probe it**. Machine-testable conditions — a
|
|
211
|
+
version on trunk, a published package, a CI run history, an advisory's patched status — rot
|
|
212
|
+
fastest and are cheapest to check. A human decision is not machine-testable; leave it.
|
|
213
|
+
3. **Classify with `classifyPreWorkCandidate(...)`** from `scripts/intake-blocker-reprobe.mjs`. A
|
|
214
|
+
discharge with no recorded evidence is not a discharge, and neither is a candidate nothing
|
|
215
|
+
probed this cycle — the helper refuses both.
|
|
216
|
+
4. **Record the result on the issue either way** via `formatReprobeNote(...)` as a comment, so the
|
|
217
|
+
next cycle reads the answer rather than re-deriving it. Keep it idempotent.
|
|
218
|
+
5. **On `selectable: true`**, relabel `$BLOCKED → $READY` with the discharging evidence in the same
|
|
219
|
+
comment, and treat it as an ordinary candidate. On anything else, leave it where it is.
|
|
170
220
|
|
|
171
221
|
#### 2b. Lifecycle-label trust resolution (bot-authored labels are not signals)
|
|
172
222
|
|
|
@@ -251,7 +301,7 @@ GitHub Issues live in one repo by definition, so the scanned repo's issues are u
|
|
|
251
301
|
- **Container visibility is allowed.** A multi-repo Epic / Story / Spike may legitimately carry multiple `repo:<name>` labels for operator visibility. Do not split or claim it here; leave the repo markers intact and fall through to the leaf-only gate, which repairs the stale build-ready label instead of dispatching the container.
|
|
252
302
|
- **Multi-repo leaf → split, never claim.** Run the `repo-scope-split` work-time procedure into single-repo siblings, each created **build-ready** (`build_ready: true`) and stamped with its own `repo:<name>`; the current repo's sibling becomes a normal candidate.
|
|
253
303
|
- **Single-repo leaf for the current repo** → fall through to 3a (leaf-only gate) and 3b (claim).
|
|
254
|
-
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the
|
|
304
|
+
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the candidate set is exhausted — exit cleanly on the denominator-stated summary, naming the current repo alongside the swept lanes.
|
|
255
305
|
|
|
256
306
|
#### 3a. Leaf-only claim gate (repair containers)
|
|
257
307
|
|
|
@@ -126,7 +126,7 @@ matches the mode this cycle ran in: **`intake-prd`** (PRD-side dispatch) or **`i
|
|
|
126
126
|
|
|
127
127
|
| This cycle's exit path | Run outcome |
|
|
128
128
|
|---|---|
|
|
129
|
-
| Empty
|
|
129
|
+
| Empty pre-work set — the idle case (step 3), nothing to claim. **Must state its denominator** (see below) | `nothing-needed` |
|
|
130
130
|
| A PRD routed to `Blocked` (clarifying questions) or `Ticketed`; a build ticket claimed and dispatched | `candidate-proposed` |
|
|
131
131
|
| A build cycle that shipped and verified (merged PR + evidence), or a shipped PRD moved to `verified` | `change-proved` |
|
|
132
132
|
| A protected deployment (or other autonomy boundary the lifecycle hits) waiting on a human approval | `approval-requested` |
|
|
@@ -151,6 +151,22 @@ node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
|
151
151
|
--runbook .lisa/automations/intake-tickets.runbook.md [--ref <item-url>]...
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
+
**A dry build lane must say what it looked in.** A `nothing-needed` run on `intake-tickets`
|
|
155
|
+
additionally passes `--denominator` — the JSON from `buildIntakeDenominator()` in
|
|
156
|
+
`scripts/intake-prework-denominator.mjs` — and the recorder **refuses the row without it**. This is
|
|
157
|
+
not decoration: 31 consecutive cycles reported a dry lane against a queue holding 61 unswept
|
|
158
|
+
pre-work rows, and every one of those records was honest. A missing row is noisy on inspection; a
|
|
159
|
+
wrong denominator is silent forever and looks exactly like a healthy dry queue. Stating the swept
|
|
160
|
+
set is what turns the silent wrong answer into an inspectable one (#2657).
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/automation-run-record.mjs" \
|
|
164
|
+
--loop-id intake-tickets --outcome nothing-needed \
|
|
165
|
+
--summary "Nothing ready to build on team ENG. Looked in every lane holding work that has not started — Backlog (20), Todo (17), Ready (2), Blocked (61) — 100 items checked out of 343 still open." \
|
|
166
|
+
--denominator "$DENOMINATOR_JSON" \
|
|
167
|
+
--runbook .lisa/automations/intake-tickets.runbook.md
|
|
168
|
+
```
|
|
169
|
+
|
|
154
170
|
If `${CLAUDE_PLUGIN_ROOT}` is unset, resolve the plugin scripts directory directly — the built copy
|
|
155
171
|
`plugins/lisa/scripts/automation-run-record.mjs` or the source
|
|
156
172
|
`plugins/src/base/scripts/automation-run-record.mjs`. If recording still fails, **degrade, never
|
|
@@ -85,7 +85,7 @@ The only legitimate reasons to stop early:
|
|
|
85
85
|
|
|
86
86
|
- Missing project key / JQL or required configuration. Surface the missing value and exit.
|
|
87
87
|
- Workflow misconfigured (pre-flight check finds `$CLAIMED` or `$DONE` not reachable, or `$READY` status absent). Surface and exit.
|
|
88
|
-
- Empty
|
|
88
|
+
- Empty pre-work set. Exit cleanly on the denominator-stated summary from `summarizeDryLane` — which names every lane swept, its count, and the open total. A bare "nothing to do" is not an acceptable exit: it is indistinguishable from a wrong denominator (#2657).
|
|
89
89
|
|
|
90
90
|
## Lifecycle assumed
|
|
91
91
|
|
|
@@ -130,11 +130,59 @@ JQL="${BASE_JQL} ORDER BY priority DESC, created ASC"
|
|
|
130
130
|
|
|
131
131
|
4. Confirm the configured Atlassian site by invoking `lisa-atlassian-access` `operation: list-sites` (it enforces connection match against `.lisa.config.json`).
|
|
132
132
|
|
|
133
|
-
### Phase 2 —
|
|
133
|
+
### Phase 2 — Sweep every pre-work status by CATEGORY
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
**Sweep by `statusCategory`, never by a roster of status names.** JIRA groups statuses into three
|
|
136
|
+
categories — `To Do` (`new`), `In Progress` (`indeterminate`), `Done` (`done`) — and a `Blocked`
|
|
137
|
+
status is not a fourth category: a project that wants one models it under **`To Do`**, i.e. work
|
|
138
|
+
that was never started. Keying the candidate set on status *names* therefore omits every pre-work
|
|
139
|
+
lane a project invented, and the scanner reports an empty queue over a full one. The same defect on
|
|
140
|
+
the Linear side produced 31 consecutive false "dry lane" cycles (#2657).
|
|
136
141
|
|
|
137
|
-
|
|
142
|
+
1. Run the ready query first: `lisa-atlassian-access` `operation: search-issues jql: "<JQL>"`. This
|
|
143
|
+
is the human-flipped lane and it is worked first.
|
|
144
|
+
2. Then sweep the rest of pre-work with the same base JQL, swapping the status clause for
|
|
145
|
+
`statusCategory = "To Do"`, and read the **total open** count (`statusCategory != Done`). The
|
|
146
|
+
open total is what makes an omitted lane arithmetically visible.
|
|
147
|
+
3. Page to exhaustion — a single unpaged `search-issues` call is not a count.
|
|
148
|
+
|
|
149
|
+
Capture each ticket's: key, summary, issue type, priority, assignee, parent (epic), status (with
|
|
150
|
+
its category), labels, components.
|
|
151
|
+
|
|
152
|
+
Build the denominator with the shared helper, which owns the pre-work vocabulary so no two
|
|
153
|
+
scanners can disagree about what counts:
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
buildIntakeDenominator({ lanes: [{name: "<status>", type: "<statusCategory>", count: <rows>}, …],
|
|
157
|
+
totalOpen: <statusCategory != Done count> })
|
|
158
|
+
summarizeDryLane(denominator, { queue: "<project key>" })
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The helper accepts JIRA's category vocabulary directly (`new` / `To Do` normalize to pre-work).
|
|
162
|
+
|
|
163
|
+
**Every candidate outside `$READY` must clear Phase 2.5 before it is treated as a candidate at
|
|
164
|
+
all.** If nothing survives, exit on the denominator-stated summary from `summarizeDryLane` — never a
|
|
165
|
+
bare "nothing to do". The run recorder rejects a dry build-intake run that does not name what it
|
|
166
|
+
swept (see `automation-runbook-contract`).
|
|
167
|
+
|
|
168
|
+
### Phase 2.5 — Re-probe the blockers instead of inheriting them
|
|
169
|
+
|
|
170
|
+
A blocker is a **claim with a timestamp, not a fact** — it goes stale the moment its condition comes
|
|
171
|
+
true, and nothing re-read one before this phase. For each pre-work candidate outside `$READY`:
|
|
172
|
+
|
|
173
|
+
1. **Human gate first, and it is absolute.** A ticket carrying the configured human-needed label
|
|
174
|
+
(`jira.labels.human_needed`, default `Human Needed`) or a `[lisa-human-gate]` marker in its
|
|
175
|
+
description is **never** auto-selected, whatever any probe says.
|
|
176
|
+
2. **Extract the stated discharge condition**, then **probe it**. Machine-testable conditions — a
|
|
177
|
+
version on trunk, a published package, a CI run history, an advisory's patched status — rot
|
|
178
|
+
fastest and are cheapest to check. A human decision is not machine-testable; leave it.
|
|
179
|
+
3. **Classify with `classifyPreWorkCandidate(...)`** from
|
|
180
|
+
`scripts/intake-blocker-reprobe.mjs`. A discharge with no recorded evidence is not a discharge,
|
|
181
|
+
and neither is a candidate nothing probed this cycle — the helper refuses both.
|
|
182
|
+
4. **Record the result on the ticket either way** via `formatReprobeNote(...)` as a comment, so the
|
|
183
|
+
next cycle reads the answer rather than re-deriving it. Keep it idempotent.
|
|
184
|
+
5. **On `selectable: true`**, transition the ticket to `$READY` with the discharging evidence in the
|
|
185
|
+
same comment, and treat it as an ordinary candidate. On anything else, leave it where it is.
|
|
138
186
|
|
|
139
187
|
### Phase 3 — Process the first eligible ready ticket
|
|
140
188
|
|
|
@@ -149,7 +197,7 @@ A JIRA project can oversee multiple repos (`frontend` / `backend` / `infrastruct
|
|
|
149
197
|
- **Unlabeled** → determine the target repo(s) from the ticket (description, AC, technical approach) confirmed against the code surfaces, then **stamp** `repo:<name>` via `lisa-atlassian-access` `operation: write-ticket` (add the label / set the component) so later cycles filter cheaply; re-apply with the now-known repo.
|
|
150
198
|
- **Multi-repo leaf → split, never claim.** Run the `repo-scope-split` work-time procedure to break it into single-repo siblings, each created **build-ready** (`build_ready: true`) and stamped with its own `repo:<name>`; the current repo's sibling becomes a normal candidate.
|
|
151
199
|
- **Single-repo leaf for the current repo** → fall through to 3a (leaf-only gate) and 3b (claim).
|
|
152
|
-
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the
|
|
200
|
+
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the candidate set is exhausted — exit cleanly on the denominator-stated summary, naming the current repo alongside the swept lanes.
|
|
153
201
|
|
|
154
202
|
#### 3a. Leaf-only claim gate (skip / safe-block containers)
|
|
155
203
|
|
|
@@ -99,7 +99,7 @@ The only legitimate reasons to stop early:
|
|
|
99
99
|
|
|
100
100
|
- Missing team key or required configuration. Surface and exit.
|
|
101
101
|
- Workflow states not yet adopted (the `ready` state does not exist on the team). Surface and exit with an Adoption hint pointing at `/lisa:setup:linear`.
|
|
102
|
-
- Empty
|
|
102
|
+
- Empty pre-work set. Exit cleanly on the denominator-stated summary from `summarizeDryLane` — which names every lane swept, its count, and the open total. A bare "nothing to do" is not an acceptable exit: it is indistinguishable from a wrong denominator (#2657).
|
|
103
103
|
|
|
104
104
|
## Lifecycle assumed
|
|
105
105
|
|
|
@@ -112,7 +112,7 @@ ready → claimed → review → done(env-keyed) (downstream)
|
|
|
112
112
|
|
|
113
113
|
(Defaults: `Ready` / `In Progress` / `In Review` / `On Dev`/`On Stg`/`Done`.)
|
|
114
114
|
|
|
115
|
-
This skill ONLY transitions `$READY → $CLAIMED` on claim, and `$CLAIMED → $DONE` on completion. It never touches the terminal production `done
|
|
115
|
+
This skill ONLY transitions `$READY → $CLAIMED` on claim, and `$CLAIMED → $DONE` on completion. It never touches the terminal production `done` or `$REVIEW` (owned by the lifecycle / `lisa-linear-evidence`). It never *sets* `$BLOCKED` either — that stays owned by the pre-flight gate — but Phase 2.5 does move an Issue **out** of a pre-work blocked lane back to `$READY` when it re-probes the Issue's own stated discharge condition and finds it no longer holds, recording the discharging evidence on the Issue.
|
|
116
116
|
|
|
117
117
|
**Pre-flight check**: at start of each cycle, confirm `$READY`, `$CLAIMED`, and the relevant `$DONE` variants exist on the team via `lisa-linear-access operation: list-workflow-states`. If `$READY` is missing, stop and report adoption needed. **Unlike labels, a missing state cannot be created on demand here** — a workflow state is team configuration with a `type` and a board position, and guessing either would put an Issue somewhere a human did not sanction. Any missing state is a setup defect: report it, name the role and the expected state, and point at `/lisa:setup:linear`.
|
|
118
118
|
|
|
@@ -125,15 +125,56 @@ This skill ONLY transitions `$READY → $CLAIMED` on claim, and `$CLAIMED → $D
|
|
|
125
125
|
- Literal `linear` → fall back to `linear.teamKey` from config.
|
|
126
126
|
2. Resolve team ID via `lisa-linear-access operation: list-teams({query: <teamKey>})`.
|
|
127
127
|
|
|
128
|
-
### Phase 2 —
|
|
128
|
+
### Phase 2 — Sweep every pre-work lane by state TYPE
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
**Sweep by `type`, never by a roster of state names.** Linear state types are `backlog | unstarted | started | completed | canceled`, and there is no `blocked` type — a team that wants a `Blocked` lane models it as **`unstarted`**, i.e. work that was *never started*. Selecting candidates from a hardcoded `Backlog / Todo / Ready` name list therefore omits an entire pre-work lane and reports an empty queue over a full one. Measured on one team: the name sweep saw **39 of 343 open rows**; the type sweep sees **100**, and the 61-row difference produced 31 consecutive false "dry lane" cycles (#2657).
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
1. List the team's workflow states via `lisa-linear-access operation: list-workflow-states`.
|
|
133
|
+
2. Keep every state whose `type` is `backlog` or `unstarted` — that is the pre-work set. `$READY` is one member of it, not the whole of it.
|
|
134
|
+
3. Query each pre-work state: `lisa-linear-access operation: list-issues({team: <teamId>, state: "<state>"})`, paging to `hasNextPage=false`. Linear's GraphQL complexity ceiling silently truncates at `first: 250`, so a single unpaged call is not a count.
|
|
135
|
+
4. Also read the **total open** count for the team (every state whose `type` is not `completed` / `canceled`). This number is what makes an omitted lane arithmetically visible.
|
|
136
|
+
|
|
137
|
+
Capture each Issue's: identifier, title, type label, priority, assignee, project, state (with its `type`), labels, description summary.
|
|
138
|
+
|
|
139
|
+
Build the denominator with the shared helper, which owns the type vocabulary so no two scanners can disagree about what counts as pre-work:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node -e '
|
|
143
|
+
import("'"${CLAUDE_PLUGIN_ROOT:-plugins/src/base}"'/scripts/intake-prework-denominator.mjs").then(m => {
|
|
144
|
+
const d = m.buildIntakeDenominator({ lanes: JSON.parse(process.argv[1]), totalOpen: Number(process.argv[2]) });
|
|
145
|
+
console.log(JSON.stringify(d));
|
|
146
|
+
console.log(m.summarizeDryLane(d, { queue: process.argv[3] }));
|
|
147
|
+
});' "$LANES_JSON" "$TOTAL_OPEN" "team $TEAM_KEY"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`$LANES_JSON` is `[{"name":"<state>","type":"<state.type>","position":<state.position>,"count":<open rows>}, …]` for **every** state on the team, pre-work and not — the helper does the selecting.
|
|
151
|
+
|
|
152
|
+
**Candidate order.** Work `$READY` first (it is the human-flipped signal), then the remaining pre-work lanes oldest-first. Every candidate outside `$READY` must clear Phase 2.5 before it is treated as a candidate at all.
|
|
133
153
|
|
|
134
154
|
> **No query-time repo pre-filter here (by design).** Unlike `lisa-jira-build-intake`, which narrows its JQL with `AND (labels = "repo:<current>" OR labels IS EMPTY)` (the query-time arm of `repo-scope-split`), the Linear `list_issues` label filter is an AND-of-labels and cannot express "current-repo **or** unlabeled" in one query. Adding `repo:<current>` to this query would strand unlabeled Issues the determine + stamp path must see. So the Linear scanner keeps this query broad and relies on the per-candidate 3a.0 gate below for repo scoping. (The `state` filter above is orthogonal to that — it narrows the lifecycle lane, not the repo, and is a single-valued equality so it has none of the AND-of-labels problem.)
|
|
135
155
|
|
|
136
|
-
If empty,
|
|
156
|
+
If every pre-work lane is empty, or nothing survives Phase 2.5, exit on the **denominator-stated** summary from `summarizeDryLane` — never a bare "nothing to do". See "Run outcome" below; the run recorder rejects a dry build-intake run that does not name what it swept.
|
|
157
|
+
|
|
158
|
+
### Phase 2.5 — Re-probe the blockers instead of inheriting them
|
|
159
|
+
|
|
160
|
+
A blocker is a **claim with a timestamp, not a fact**. It is written once and goes stale the moment its condition comes true — a dependency lands on trunk, an advisory gets patched, a package publishes. Nothing re-read one before this phase, so a discharged blocker held its Issue out of the queue indefinitely. Measured: one Issue's stated condition went true ~15 hours before anything noticed.
|
|
161
|
+
|
|
162
|
+
For each pre-work candidate that is **not** in `$READY`:
|
|
163
|
+
|
|
164
|
+
1. **Human gate first, and it is absolute.** An Issue carrying the configured human-needed label (`linear.labels.build.human_needed`, default `human-needed`) or a `[lisa-human-gate]` marker in its description is **never** auto-selected, whatever any probe says. Skip it and move on.
|
|
165
|
+
2. **Extract the stated discharge condition** from the description or the most recent blocking comment — the sentence naming what has to become true.
|
|
166
|
+
3. **Probe it.** Machine-testable conditions are the ones that rot fastest and are cheapest to check: a version on trunk (`git show origin/<trunk>:<manifest>`), a published package, a run history (`gh run list`), an advisory's patched status. A condition that is a human decision is not machine-testable — leave it and move on.
|
|
167
|
+
4. **Classify with the shared helper** so the ordering and the evidence requirement cannot drift per vendor:
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
classifyPreWorkCandidate({ laneType, labels, body, humanNeededLabel, statedBlocker, probe })
|
|
171
|
+
→ { selectable, reason, humanGated, evidence }
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
A discharge with **no recorded evidence is not a discharge** — the helper refuses it. So is a candidate nothing probed this cycle.
|
|
175
|
+
|
|
176
|
+
5. **Record the result on the Issue either way**, via `lisa-linear-access operation: save-comment` using `formatReprobeNote(...)`, so the next cycle reads the answer rather than re-deriving it. Keep it idempotent — skip the post when an identical note already exists.
|
|
177
|
+
6. **On `selectable: true`**, move the Issue to `$READY` (recording the discharging evidence in the same comment) and treat it as an ordinary candidate from Phase 3 onward. On anything else, leave the Issue exactly where it is.
|
|
137
178
|
|
|
138
179
|
### Phase 3 — Process the first eligible ready Issue
|
|
139
180
|
|
|
@@ -148,7 +189,7 @@ A Linear team can oversee multiple repos (`frontend` / `backend` / `infrastructu
|
|
|
148
189
|
- **Unlabeled** → determine the target repo(s) from the Issue + code surfaces, then **stamp** `repo:<name>` via `lisa-linear-access operation: save-issue` (resolve/create the label via `list_issue_labels`/`create_issue_label`) so later cycles filter cheaply; re-apply with the now-known repo.
|
|
149
190
|
- **Multi-repo leaf → split, never claim.** Run the `repo-scope-split` work-time procedure into single-repo siblings, each created **build-ready** (`build_ready: true`) and stamped with its own `repo:<name>`; the current repo's sibling becomes a normal candidate.
|
|
150
191
|
- **Single-repo leaf for the current repo** → fall through to 3a (leaf-only gate) and 3b (claim).
|
|
151
|
-
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the
|
|
192
|
+
4. Continue until a claimable current-repo leaf is found (claim it; one per cycle) or the candidate set is exhausted — exit cleanly on the denominator-stated summary, naming the current repo alongside the swept lanes.
|
|
152
193
|
|
|
153
194
|
#### 3a. Leaf-only claim gate (skip / safe-block containers)
|
|
154
195
|
|
|
@@ -42,6 +42,30 @@ This is the claim-time arm of the rule. Its siblings are the write-time labeling
|
|
|
42
42
|
|
|
43
43
|
The shim never needs to inspect the item itself — it forwards `$ARGUMENTS` verbatim and the resolved vendor scanner runs its Phase 3a gate before any claim.
|
|
44
44
|
|
|
45
|
+
## Pre-work denominator contract (forwarded to every vendor)
|
|
46
|
+
|
|
47
|
+
Also part of the build-intake API, and the reason a dry queue can be believed. Two obligations,
|
|
48
|
+
uniform across `jira`, `github`, and `linear`:
|
|
49
|
+
|
|
50
|
+
1. **Sweep every pre-work lane by its machine-readable category, never by a roster of lane names.**
|
|
51
|
+
No tracker has a `blocked` category: Linear models a `Blocked` state as `unstarted`, JIRA models
|
|
52
|
+
a `Blocked` status under `To Do`, and GitHub has no category at all, so its lanes come from
|
|
53
|
+
configured roles. In every case a `Blocked` lane is *pre-work* — items that were never started,
|
|
54
|
+
each carrying a written blocker. Keying selection on names omits it silently.
|
|
55
|
+
2. **A `nothing-needed` run states its denominator** — which lanes were swept, how many rows each
|
|
56
|
+
held, and the total open. This is enforced, not requested: `automation-run-record.mjs` refuses a
|
|
57
|
+
`nothing-needed` row for `intake-tickets` without a `--denominator`.
|
|
58
|
+
|
|
59
|
+
Shared helpers own both, so the vocabulary cannot drift per vendor:
|
|
60
|
+
`scripts/intake-prework-denominator.mjs` (`buildIntakeDenominator`, `summarizeDryLane`) and
|
|
61
|
+
`scripts/intake-blocker-reprobe.mjs` (`classifyPreWorkCandidate`, `formatReprobeNote`). The
|
|
62
|
+
blocker re-probe carries an absolute human gate: an item carrying the configured human-needed label
|
|
63
|
+
or a `[lisa-human-gate]` marker is never auto-selected, whatever a probe returns.
|
|
64
|
+
|
|
65
|
+
Measured: sweeping one team by lane name saw 39 of 343 open rows; by category it sees 100. The
|
|
66
|
+
61-row gap produced 31 consecutive false "dry lane" cycles, every record honest and every
|
|
67
|
+
conclusion wrong (#2657).
|
|
68
|
+
|
|
45
69
|
## Repo-scope claim contract (forwarded to every vendor)
|
|
46
70
|
|
|
47
71
|
Equally part of the build-intake API, and forwarded identically: when the tracker oversees multiple repos, each vendor scanner claims only tickets for the repo it is running in. Per the `repo-scope-split` rule's "Claim-time repo scoping" section, before the leaf-only gate each scanner (Phase 3a.0) resolves the current repo (`config-resolution` "Repo scoping": `repo` → `github.repo` → git remote basename), then for each ready candidate: skips a ticket labeled `repo:<other>`, determines + stamps `repo:<name>` on an unlabeled one, splits a multi-repo leaf into single-repo build-ready siblings, and claims only a single-repo leaf for the current repo. This shim does not re-implement the gate — it relies on the vendor scanner's Phase 3a.0 — but the contract is uniform across `jira`, `github`, and `linear` so behavior never drifts by tracker. It is the claim-time complement to the write-time S10 scope gate (`lisa-tracker-validate`) and `task-decomposition` step 1.5; all cite `repo-scope-split`.
|
|
@@ -125,6 +125,25 @@ recovery-required Lost access to the tracker; filed #1812 with the one decisio
|
|
|
125
125
|
policy-obsolete No work has reached this queue in 30 days; proposed teardown in #1813.
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
### `nothing-needed` states its denominator
|
|
129
|
+
|
|
130
|
+
A queue-scanning loop that reports nothing is only as trustworthy as the set it looked at, and a
|
|
131
|
+
summary of the form "scanned N items" hides the one thing worth checking: **which lanes N came
|
|
132
|
+
from.** So a `nothing-needed` summary from a queue scanner names the lanes it swept, the count in
|
|
133
|
+
each, and the total open — `Looked in every lane holding work that has not started — Backlog (20),
|
|
134
|
+
Todo (17), Ready (2), Blocked (61) — 100 items checked out of 343 still open.`
|
|
135
|
+
|
|
136
|
+
This is a measured requirement, not a style preference. One build lane reported 31 consecutive dry
|
|
137
|
+
cycles over a queue holding 61 unswept rows, at least one of them buildable for ~15 hours. Every
|
|
138
|
+
one of the 31 records was honest and every conclusion was false, because the scanner never said
|
|
139
|
+
what its denominator was. **A missing run record is noisy on inspection; a wrong denominator is
|
|
140
|
+
silent forever and looks exactly like a healthy dry queue.** Stating the swept set is what makes
|
|
141
|
+
the second failure mode visible at all.
|
|
142
|
+
|
|
143
|
+
The build-intake loop enforces this rather than requesting it: `automation-run-record.mjs` refuses
|
|
144
|
+
a `nothing-needed` row for `intake-tickets` unless the call carries a `--denominator` built by
|
|
145
|
+
`scripts/intake-prework-denominator.mjs`.
|
|
146
|
+
|
|
128
147
|
**No silent exit.** Every run posts its outcome and its one-line summary before stopping —
|
|
129
148
|
including the trivial early terminations (empty queue, nothing eligible, already-claimed) — so
|
|
130
149
|
there is no silent exit for an operator to misread as health. A run that ends without a
|
|
@@ -53,6 +53,8 @@ Exactly one per run:
|
|
|
53
53
|
Health and operator action are **orthogonal** — a healthy run can still need an answer:
|
|
54
54
|
|
|
55
55
|
- `nothing-needed` — the loop ran and found nothing to act on. **Healthy.** Operator action: none.
|
|
56
|
+
A queue scanner's `nothing-needed` summary must name the lanes it swept and the total open — a dry
|
|
57
|
+
queue and a wrong denominator are otherwise indistinguishable. See the reference body.
|
|
56
58
|
- `candidate-proposed` — the loop proposed work (ticket, PRD, recommendation). **Healthy.** Operator
|
|
57
59
|
action: review the proposed item and flip it ready when you want it built.
|
|
58
60
|
- `change-proved` — the loop made a change and proved it with evidence. **Healthy.** Operator
|