@dev-loops/core 1.0.2-pre.0 → 1.0.2-slim.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/package.json +2 -1
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +29 -36
- package/src/config/config.mjs +388 -787
- package/src/github/comment-id-guard.mjs +2 -2
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +2 -2
- package/src/loop/gate-fanin.mjs +252 -442
- package/src/loop/handoff-envelope.mjs +19 -19
- package/src/loop/issue-refinement-artifact.mjs +158 -252
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +37 -37
- package/src/loop/queue-board-sync.mjs +16 -55
- package/src/loop/review-dispatch-plan.mjs +60 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +39 -69
- package/src/loop/steering.mjs +16 -68
- package/src/projects/list-queue-items.mjs +16 -146
- package/src/projects/move-queue-item.mjs +15 -141
- package/src/projects/projects-access.mjs +202 -0
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Deterministic issue refinement-artifact detection.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* (the doc carries the matrix). Interactive issue-side Acceptance criteria /
|
|
12
|
-
* Definition of done CHECKLISTS are NO LONGER required merely to satisfy
|
|
13
|
-
* detection (#1951 AC1): the matrix is the authoritative issue artifact, and
|
|
14
|
-
* the PR carries the derived self-contained list-form AC/DoD checklists
|
|
15
|
-
* (`derivePrChecklistsFromIssueMatrix`; the PR body is validated by
|
|
4
|
+
* The authoritative refined-issue artifact is the semantic AC→DoD mapping
|
|
5
|
+
* MATRIX (a two-column table mapping each acceptance-criterion outcome to its
|
|
6
|
+
* required completion evidence) plus an explicit Non-goals section — or a
|
|
7
|
+
* linked refinement doc that is a complete artifact on its own (the doc carries
|
|
8
|
+
* the matrix). Interactive issue-side AC/DoD checklists are NOT a substitute
|
|
9
|
+
* for the matrix; the PR carries the derived self-contained list-form
|
|
10
|
+
* checklists (`derivePrChecklistsFromIssueMatrix`; the PR body is validated by
|
|
16
11
|
* `validateTrackerBackedPrBodySpec`, never this predicate).
|
|
17
12
|
*
|
|
18
13
|
* Detection validates the structural PRESENCE and SHAPE of the mapping table,
|
|
19
|
-
* not its semantic truthfulness (
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Fix) cause the draft gate to post `verdict=blocked` with the
|
|
26
|
-
* `missing_refinement_artifact` finding.
|
|
27
|
-
*
|
|
28
|
-
* Migration (#1951 AC7/D7): existing checklist-bearing issues stay readable —
|
|
29
|
-
* the parser still extracts their AC/DoD checklist content — but a body that
|
|
30
|
-
* carries only checklists and no mapping matrix now fails closed with
|
|
31
|
-
* `missing_ac_dod_matrix` and is re-grilled (loop-grill synthesizes the
|
|
32
|
-
* matrix) rather than being silently grandfathered. No compatibility alias is
|
|
33
|
-
* retained.
|
|
14
|
+
* not its semantic truthfulness (a reviewer responsibility). A matrix that is
|
|
15
|
+
* absent, empty, malformed, or identifier-only fails closed with the matching
|
|
16
|
+
* finding (`missing_ac_dod_matrix`, `malformed_ac_dod_matrix`,
|
|
17
|
+
* `missing_explicit_non_goals`, or `missing_refinement_artifact`). A body
|
|
18
|
+
* carrying only checklists and no matrix fails closed and is re-grilled; no
|
|
19
|
+
* compatibility alias is retained.
|
|
34
20
|
*/
|
|
35
21
|
import { existsSync } from "node:fs";
|
|
36
22
|
import path from "node:path";
|
|
@@ -57,11 +43,9 @@ export const REFINEMENT_SOURCE = Object.freeze({
|
|
|
57
43
|
|
|
58
44
|
const REFINEMENT_ARTIFACT_FINDING = "missing_refinement_artifact";
|
|
59
45
|
|
|
60
|
-
// REFINEMENT_ARTIFACT_SOURCES: the
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// this list is the shape of a COMPLETE artifact, not a menu where any one
|
|
64
|
-
// entry suffices.
|
|
46
|
+
// REFINEMENT_ARTIFACT_SOURCES: the shape of a COMPLETE refinement artifact,
|
|
47
|
+
// not a menu where any one entry suffices. A linked refinement doc
|
|
48
|
+
// remains a complete artifact on its own.
|
|
65
49
|
export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
66
50
|
"AC→DoD mapping matrix (a two-column table)",
|
|
67
51
|
"explicit Non-goals section",
|
|
@@ -69,30 +53,25 @@ export const REFINEMENT_ARTIFACT_SOURCES = Object.freeze([
|
|
|
69
53
|
]);
|
|
70
54
|
|
|
71
55
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* (`PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.code`) so both spec surfaces
|
|
76
|
-
* name the missing invariant identically.
|
|
56
|
+
* Finding: refinement artifact present but no explicit Non-goals section.
|
|
57
|
+
* Mirrors the PR-path code (`PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.code`)
|
|
58
|
+
* so both spec surfaces name the missing invariant identically.
|
|
77
59
|
*/
|
|
78
60
|
export const MISSING_EXPLICIT_NON_GOALS_FINDING = "missing_explicit_non_goals";
|
|
79
61
|
|
|
80
62
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* issue-side AC/DoD checklists are not a substitute for it. Fails closed so
|
|
86
|
-
* the issue is re-grilled to add the matrix.
|
|
63
|
+
* Finding: refinement content present but NO authoritative AC→DoD mapping
|
|
64
|
+
* matrix table. The mapping table is the authoritative issue artifact;
|
|
65
|
+
* issue-side checklists are not a substitute. Fails closed so the issue is
|
|
66
|
+
* re-grilled to add the matrix.
|
|
87
67
|
*/
|
|
88
68
|
export const MISSING_AC_DOD_MATRIX_FINDING = "missing_ac_dod_matrix";
|
|
89
69
|
|
|
90
70
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* truthfulness of the mapping stays a reviewer responsibility.
|
|
71
|
+
* Finding: an AC→DoD mapping table is present but empty (header/separator only)
|
|
72
|
+
* or identifier-only/tautological (cells such as `AC1 → D1` with no concrete
|
|
73
|
+
* criterion or evidence prose). Structural shape validation only — semantic
|
|
74
|
+
* truthfulness stays a reviewer responsibility.
|
|
96
75
|
*/
|
|
97
76
|
export const MALFORMED_AC_DOD_MATRIX_FINDING = "malformed_ac_dod_matrix";
|
|
98
77
|
|
|
@@ -104,47 +83,35 @@ export const MALFORMED_AC_DOD_MATRIX_FINDING = "malformed_ac_dod_matrix";
|
|
|
104
83
|
* - one DoD-style section (DoD or Definition of Done)
|
|
105
84
|
*/
|
|
106
85
|
const ACCEPTANCE_SECTION_PATTERNS = Object.freeze([
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// (
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// spelled-out phrase). The anchor stays distinct from the alias family below
|
|
113
|
-
// (`/^ac\b/`), so the precedence contract is unchanged: a spelled-out
|
|
114
|
-
// canonical heading always outranks an abbreviation-shaped alias heading.
|
|
86
|
+
// Index 0 is the exact-canonical ANCHOR (`^acceptance criteria\b`); the rest
|
|
87
|
+
// are aliases. A decorated-variant canonical heading (`## Acceptance criteria
|
|
88
|
+
// (v2)`) still lands in the exact bucket rather than matching no pattern. The
|
|
89
|
+
// anchor stays distinct from the `/^ac\b/` alias, so a spelled-out canonical
|
|
90
|
+
// heading always outranks an abbreviation-shaped alias heading.
|
|
115
91
|
/^acceptance criteria\b.*$/i,
|
|
116
92
|
/^ac\b.*$/i,
|
|
117
93
|
]);
|
|
118
94
|
|
|
119
95
|
const DOD_SECTION_PATTERNS = Object.freeze([
|
|
120
|
-
// Same anchor-family widening as the AC family
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// disarms the PR-side DoD read and false-blocks the issue side).
|
|
96
|
+
// Same anchor-family widening as the AC family. A decorated-variant alias
|
|
97
|
+
// heading (`## DoD (v2)`) must land in the alias bucket, not in no bucket: a
|
|
98
|
+
// `$`-anchored alias silently disarms the PR-side DoD read and false-blocks
|
|
99
|
+
// the issue side.
|
|
125
100
|
/^definition of done\b.*$/i,
|
|
126
101
|
/^done\b.*$/i,
|
|
127
102
|
/^dod\b.*$/i,
|
|
128
103
|
]);
|
|
129
104
|
|
|
130
105
|
/**
|
|
131
|
-
* Normalize a heading name before section-pattern matching
|
|
132
|
-
*
|
|
133
|
-
* headings
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* still matches its alias family. Strips: surrounding emphasis runs of any
|
|
141
|
-
* of `*`/`_` (bold `**`/`__` and single-char italic `*`/`_` alike, #1877
|
|
142
|
-
* round-7), surrounding backtick runs, trailing `:` and surrounding
|
|
143
|
-
* whitespace.
|
|
144
|
-
* Deliberately NOT touched: interior text (a real `AC (v2) - final` name keeps
|
|
145
|
-
* its interior), leading `#` (ATX markers never reach `match[2]`), and any
|
|
146
|
-
* decoration a section pattern itself could rely on (none does — every family
|
|
147
|
-
* anchors at the name's start).
|
|
106
|
+
* Normalize a heading name before section-pattern matching. Strip
|
|
107
|
+
* harmless decoration once, at the parse boundary, so decorated canonical
|
|
108
|
+
* headings (`## **Acceptance criteria**`, `## Acceptance criteria:`) still
|
|
109
|
+
* match their pattern family instead of silently disarming the AC/DoD reads.
|
|
110
|
+
* Exact-vs-alias precedence stays intact: a normalized `Acceptance criteria`
|
|
111
|
+
* still matches the exact pattern, a decorated alias still matches its alias
|
|
112
|
+
* family. Strips surrounding emphasis/backtick runs (bold and single-char
|
|
113
|
+
* italic), trailing `:`, closing ATX `#`, and surrounding whitespace.
|
|
114
|
+
* NOT touched: interior text, and leading `#` (never reaches `match[2]`).
|
|
148
115
|
*/
|
|
149
116
|
function normalizeHeadingName(name) {
|
|
150
117
|
if (typeof name !== "string") return name;
|
|
@@ -152,30 +119,23 @@ function normalizeHeadingName(name) {
|
|
|
152
119
|
// trailing decoration first: closing `##` ATX-style, colons, whitespace
|
|
153
120
|
.replace(/\s*:*\s*$/u, "")
|
|
154
121
|
.replace(/\s*#+\s*$/u, "")
|
|
155
|
-
// surrounding emphasis/backtick runs (any length, must pair;
|
|
156
|
-
//
|
|
157
|
-
// `**`/`__`, so `## *Acceptance criteria*` and `## _Definition of done_`
|
|
158
|
-
// normalize exactly like their bold forms)
|
|
122
|
+
// surrounding emphasis/backtick runs (any length, must pair; a run may be
|
|
123
|
+
// single-char italic `*`/`_` as well as bold `**`/`__`)
|
|
159
124
|
.replace(/^[*_`]+/u, "")
|
|
160
125
|
.replace(/[*_`]+$/u, "")
|
|
161
126
|
.trim();
|
|
162
127
|
}
|
|
163
128
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// each pattern family into [exact, aliases] by convention: pattern index 0
|
|
169
|
-
// is the exact canonical match, the rest are aliases.
|
|
129
|
+
// Exact canonical headings (pattern index 0 in each family) must outrank loose
|
|
130
|
+
// aliases (`/^ac\b/`, `/^dod\b/`) so a matrix-shaped heading (`## AC/DoD
|
|
131
|
+
// matrix`) can never hijack the canonical section read. Index 0 is the exact
|
|
132
|
+
// canonical match, the rest are aliases.
|
|
170
133
|
const EXACT_PATTERN_INDEX = 0;
|
|
171
134
|
|
|
172
135
|
/**
|
|
173
|
-
* Resolve the
|
|
174
|
-
* precedence
|
|
175
|
-
*
|
|
176
|
-
* (e.g. `## AC/DoD matrix` before `## Acceptance criteria`). When no exact
|
|
177
|
-
* match exists, the first alias match is returned (alias-only bodies keep
|
|
178
|
-
* working). Returns null when no section matches at all.
|
|
136
|
+
* Resolve the section matching a heading-pattern family with exact-first
|
|
137
|
+
* precedence: the first EXACT canonical match (index 0) wins over any
|
|
138
|
+
* earlier alias-only match. Falls back to the first alias match, else null.
|
|
179
139
|
*/
|
|
180
140
|
function findSectionByPatterns(sections, patterns) {
|
|
181
141
|
const exact = patterns[EXACT_PATTERN_INDEX];
|
|
@@ -195,10 +155,9 @@ function findSectionByPatterns(sections, patterns) {
|
|
|
195
155
|
}
|
|
196
156
|
|
|
197
157
|
/**
|
|
198
|
-
* Collect ALL sections matching a heading-pattern family, exact-first ordered
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
* and union consumers (#1877 PR-body unchecked-box extraction) cannot drift.
|
|
158
|
+
* Collect ALL sections matching a heading-pattern family, exact-first ordered.
|
|
159
|
+
* Shares `findSectionByPatterns`'s precedence semantics so single-section and
|
|
160
|
+
* union consumers (PR-body unchecked-box extraction) cannot drift.
|
|
202
161
|
*/
|
|
203
162
|
function findAllSectionsByPatterns(sections, patterns) {
|
|
204
163
|
const exact = patterns[EXACT_PATTERN_INDEX];
|
|
@@ -220,28 +179,19 @@ function findAllSectionsByPatterns(sections, patterns) {
|
|
|
220
179
|
}
|
|
221
180
|
|
|
222
181
|
/**
|
|
223
|
-
* Flatten a section
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* to the next same-or-shallower heading. `parseMarkdownSections` terminates a
|
|
228
|
-
* section's `bodyLines` at ANY heading, which is correct for heading
|
|
229
|
-
* matching but hides unchecked boxes from consumers that must see ALL of a
|
|
230
|
-
* canonical section's boxes.
|
|
182
|
+
* Flatten a section into a body string that extends past `###` sub-headings
|
|
183
|
+
* by joining the section and every following DEEPER-level section up to the
|
|
184
|
+
* next same-or-shallower heading, so nested checklist items stay visible to
|
|
185
|
+
* consumers that must see ALL of a canonical section's boxes.
|
|
231
186
|
*/
|
|
232
187
|
function flattenSectionDeep(sections, startIndex) {
|
|
233
188
|
const start = sections[startIndex];
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
// (`###
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// Only already-classified bodyLines are joined — a heading can never match
|
|
241
|
-
// any line-level grammar, and real boxes under sub-headings stay visible
|
|
242
|
-
// because their bodyLines still join normally. (Keeping a marker line is
|
|
243
|
-
// unnecessary: parseChecklistItems never needed the heading boundary to
|
|
244
|
-
// track fence state — body lines carry their own fences.)
|
|
189
|
+
// Anti-spoof: the raw sub-heading NAME is NEVER re-injected into the text the
|
|
190
|
+
// checklist parser re-parses. A fence-opening name (`### ``` `) would corrupt
|
|
191
|
+
// fence state and eat real boxes (fail-open); a checkbox-shaped name
|
|
192
|
+
// (`### - [ ] fake`) would count as a phantom unchecked item (fail-closed).
|
|
193
|
+
// Only already-classified bodyLines are joined; real boxes under sub-headings
|
|
194
|
+
// stay visible because their bodyLines still join normally.
|
|
245
195
|
const parts = [start.bodyLines.join("\n")];
|
|
246
196
|
for (let i = startIndex + 1; i < sections.length; i += 1) {
|
|
247
197
|
if (sections[i].level <= start.level) break;
|
|
@@ -260,7 +210,7 @@ function flattenSectionDeep(sections, startIndex) {
|
|
|
260
210
|
* CommonMark: an N-marker fence (``` or ~~~) closes only on a line of >= N
|
|
261
211
|
* markers of the SAME char with no info string. This is the single source of
|
|
262
212
|
* truth shared by parseMarkdownSections (headings) and extractChecklistItems
|
|
263
|
-
* (checkboxes) so the two anti-spoof layers cannot drift
|
|
213
|
+
* (checkboxes) so the two anti-spoof layers cannot drift.
|
|
264
214
|
*/
|
|
265
215
|
function stepFence(fence, line) {
|
|
266
216
|
const openMatch = /^\s*(`{3,}|~{3,})/u.exec(line);
|
|
@@ -287,7 +237,7 @@ function stepFence(fence, line) {
|
|
|
287
237
|
*
|
|
288
238
|
* Headings inside a fenced code span (``` or ~~~) are NOT treated as headings —
|
|
289
239
|
* otherwise a body could spoof the refinement/spec gate with real-looking
|
|
290
|
-
* headings that carry no real spec (gate integrity
|
|
240
|
+
* headings that carry no real spec (gate integrity).
|
|
291
241
|
*/
|
|
292
242
|
export function parseMarkdownSections(body) {
|
|
293
243
|
if (typeof body !== "string" || body.length === 0) {
|
|
@@ -313,11 +263,9 @@ export function parseMarkdownSections(body) {
|
|
|
313
263
|
}
|
|
314
264
|
current = {
|
|
315
265
|
level: match[1].length,
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// longer re-injects it), so normalization is the only consumer of the
|
|
320
|
-
// capture — the raw form is not retained (no consumer reads it).
|
|
266
|
+
// Normalize the captured name so decorated canonical headings
|
|
267
|
+
// (`## **Acceptance criteria**`) match the section patterns. The raw
|
|
268
|
+
// form is never re-parsed as body text, so it is not retained.
|
|
321
269
|
name: normalizeHeadingName(match[2]),
|
|
322
270
|
bodyLines: [],
|
|
323
271
|
};
|
|
@@ -337,27 +285,21 @@ export function parseMarkdownSections(body) {
|
|
|
337
285
|
|
|
338
286
|
|
|
339
287
|
/**
|
|
340
|
-
* Parse bullet/checkbox items from a section body into item states.
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* not counted, so a section of only unfilled placeholders reports as unrefined.
|
|
351
|
-
* Code-fenced lines are skipped (same fence logic as parseMarkdownSections,
|
|
352
|
-
* issue #1025) so a body cannot spoof the AC/DoD gate with code-fenced
|
|
353
|
-
* checkboxes.
|
|
288
|
+
* Parse bullet/checkbox items from a section body into item states. A checkbox
|
|
289
|
+
* item — any GFM/CommonMark task-list marker (`-`/`*`/`+` bullets, ordered
|
|
290
|
+
* `N.`/`N)`, blockquote-nested `> - [ ]`; parity with
|
|
291
|
+
* tick-verified-checkboxes.mjs) — becomes `{ text, checked }`, where `checked`
|
|
292
|
+
* is true only for a ticked `[x]`/`[X]` marker read from the captured marker
|
|
293
|
+
* group, never a whole-line re-test. A top-level plain bullet (`- text`, dash
|
|
294
|
+
* at column 0) becomes `{ text, checked: null }` — it has no checkbox. Empty
|
|
295
|
+
* placeholders (`- [ ]` with no text) are skipped. Code-fenced lines are
|
|
296
|
+
* skipped (same fence logic as parseMarkdownSections) so a body cannot spoof
|
|
297
|
+
* the AC/DoD gate with code-fenced checkboxes.
|
|
354
298
|
*
|
|
355
|
-
* Shared by `extractChecklistItems`
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* `detectIssueRefinementArtifact`), so counting plain bullets is scoped to
|
|
360
|
-
* those sections and never affects prose sections.
|
|
299
|
+
* Shared by `extractChecklistItems` and `extractUncheckedChecklistItems` so the
|
|
300
|
+
* two never drift on what counts as an item or on the checkbox-state read.
|
|
301
|
+
* Only called on an already-recognized AC/DoD section, so counting
|
|
302
|
+
* plain bullets never affects prose sections.
|
|
361
303
|
*/
|
|
362
304
|
function parseChecklistItems(sectionBody) {
|
|
363
305
|
if (typeof sectionBody !== "string" || sectionBody.length === 0) {
|
|
@@ -374,20 +316,15 @@ function parseChecklistItems(sectionBody) {
|
|
|
374
316
|
if (step.insideFence) {
|
|
375
317
|
continue;
|
|
376
318
|
}
|
|
377
|
-
// Checklist item: GFM/CommonMark task-list markers (
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
// CAPTURED marker group of this single match — never a second whole-line
|
|
387
|
-
// re-test. An unanchored `/\[(?:[xX])\]/u.test(line)` reads an UNCHECKED box
|
|
388
|
-
// whose label text merely mentions `[x]` (e.g. `- [ ] verify [x] flags`) as
|
|
389
|
-
// checked, silently disarming the deterministic block — the exact
|
|
390
|
-
// fail-open class the marker-anchored pre-#1877 read could not produce.
|
|
319
|
+
// Checklist item: GFM/CommonMark task-list markers (`-`/`*`/`+` bullets,
|
|
320
|
+
// ordered `N.`/`N)`, blockquote-nested `> - [ ]`) — grammar parity with
|
|
321
|
+
// tick-verified-checkboxes.mjs so every form surfaced as unchecked is
|
|
322
|
+
// flippable by the tick tool. Push only when the line carries text, so
|
|
323
|
+
// empty placeholders are skipped. The tick state comes from the CAPTURED
|
|
324
|
+
// marker group, never a second whole-line re-test: an unanchored
|
|
325
|
+
// `/\[[xX]\]/.test(line)` would read an unchecked box whose label merely
|
|
326
|
+
// mentions `[x]` (`- [ ] verify [x] flags`) as checked, a fail-open the
|
|
327
|
+
// marker-anchored read cannot produce.
|
|
391
328
|
const checkboxMatch =
|
|
392
329
|
/^\s*(?:>|\s)*(?:[-*+]|\d+[.)])\s+\[([ xX])\](?:\s+(.+?))?\s*$/u.exec(line);
|
|
393
330
|
if (checkboxMatch) {
|
|
@@ -421,7 +358,7 @@ function parseChecklistItems(sectionBody) {
|
|
|
421
358
|
* are skipped. Returns the trimmed item text for each matching line; the
|
|
422
359
|
* checkbox state is not preserved (use `extractUncheckedChecklistItems` for
|
|
423
360
|
* that). Thin wrapper over `parseChecklistItems` so the text-only contract
|
|
424
|
-
* stays byte-identical to its
|
|
361
|
+
* stays byte-identical to its original shape.
|
|
425
362
|
*/
|
|
426
363
|
export function extractChecklistItems(sectionBody) {
|
|
427
364
|
return parseChecklistItems(sectionBody).map((item) => item.text);
|
|
@@ -431,7 +368,7 @@ export function extractChecklistItems(sectionBody) {
|
|
|
431
368
|
* Extract the text of UNCHECKED checkbox items (`- [ ]`) from a section body.
|
|
432
369
|
* A ticked box (`- [x]`/`- [X]`) and a plain bullet (no checkbox) are both
|
|
433
370
|
* excluded — only an actual unticked checkbox is an "unticked AC item"
|
|
434
|
-
* (
|
|
371
|
+
* (ACCEPT-CRITERIA-VERIFY-AND-REFLECT). Empty placeholders are skipped.
|
|
435
372
|
* Thin wrapper over `parseChecklistItems` so the unticked read never drifts
|
|
436
373
|
* from `extractChecklistItems` on what counts as a checklist item.
|
|
437
374
|
*/
|
|
@@ -442,7 +379,7 @@ export function extractUncheckedChecklistItems(sectionBody) {
|
|
|
442
379
|
}
|
|
443
380
|
|
|
444
381
|
// ---------------------------------------------------------------------------
|
|
445
|
-
// AC→DoD mapping matrix detection
|
|
382
|
+
// AC→DoD mapping matrix detection
|
|
446
383
|
// ---------------------------------------------------------------------------
|
|
447
384
|
// The authoritative refined-issue artifact is a semantic AC→DoD mapping table:
|
|
448
385
|
// a GFM pipe table whose rows map each acceptance-criterion outcome to its
|
|
@@ -461,14 +398,12 @@ const MATRIX_SECTION_PATTERNS = Object.freeze([
|
|
|
461
398
|
]);
|
|
462
399
|
|
|
463
400
|
// Header column families: col0 names the criterion side, col1 the evidence
|
|
464
|
-
// side.
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// explicitly name acceptance criteria AND completion evidence / DoD qualify
|
|
401
|
+
// side. Recognizes an unheaded but clearly criterion→evidence table anywhere in
|
|
402
|
+
// the body. Kept STRONG on purpose: a generic status table like
|
|
403
|
+
// `| Outcome | Done |` must NOT be mistaken for the refinement matrix — only
|
|
404
|
+
// headers naming acceptance criteria AND completion evidence / DoD qualify
|
|
469
405
|
// without a matrix heading. A matrix under a weaker header still qualifies via
|
|
470
|
-
// its `## AC / DoD matrix` heading (MATRIX_SECTION_PATTERNS)
|
|
471
|
-
// loop-grill synthesis and the epic procedure both write.
|
|
406
|
+
// its `## AC / DoD matrix` heading (MATRIX_SECTION_PATTERNS).
|
|
472
407
|
const MATRIX_CRITERION_HEADER = /\b(criteri\w*|acceptance|ac)\b/i;
|
|
473
408
|
const MATRIX_EVIDENCE_HEADER = /\b(evidence|dod|definition of done)\b/i;
|
|
474
409
|
|
|
@@ -498,13 +433,11 @@ function cellProseWordCount(cell) {
|
|
|
498
433
|
}
|
|
499
434
|
|
|
500
435
|
// A matrix data row is semantic when BOTH mapped cells carry at least one real
|
|
501
|
-
// prose word
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// is to reject bare identifiers, not to mandate a minimum verbosity (#1951
|
|
507
|
-
// draft_gate/Copilot review).
|
|
436
|
+
// prose word. This rejects identifier-only/tautological rows (`AC1 | D1`,
|
|
437
|
+
// `AC1 → D1`, `DoD`, empty cells) WITHOUT false-rejecting a terse-but-real
|
|
438
|
+
// mapping (`Feature works | Regression test added`). The threshold is
|
|
439
|
+
// deliberately >=1, not >=2: reject bare identifiers, do not mandate a minimum
|
|
440
|
+
// verbosity.
|
|
508
441
|
function rowIsSemantic(criterion, evidence) {
|
|
509
442
|
return cellProseWordCount(criterion) >= 1 && cellProseWordCount(evidence) >= 1;
|
|
510
443
|
}
|
|
@@ -623,7 +556,7 @@ export function detectAcDodMatrix(body = "") {
|
|
|
623
556
|
|
|
624
557
|
/**
|
|
625
558
|
* Project an issue's AC→DoD mapping matrix into self-contained list-form PR
|
|
626
|
-
* checklists
|
|
559
|
+
* checklists: the PR carries list-form Acceptance criteria and
|
|
627
560
|
* Definition of done checkboxes derived from the matrix — never a matrix/table,
|
|
628
561
|
* never checkboxes inside table cells. Accepts a pre-parsed `matrix` (from
|
|
629
562
|
* {@link detectAcDodMatrix}) or a raw `body` to parse. Fails closed on a
|
|
@@ -697,36 +630,21 @@ export function detectLinkedRefinementDoc(body) {
|
|
|
697
630
|
/**
|
|
698
631
|
* Detect the refinement artifact on a parsed issue body.
|
|
699
632
|
*
|
|
700
|
-
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
* `
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
* the FULL check passes (#1951: a valid AC→DoD mapping matrix plus an explicit
|
|
710
|
-
* Non-goals section, or a resolvable linked refinement doc plus Non-goals), so
|
|
711
|
-
* every `.hasACs` consumer (enqueue gate, draft gate, parked-items discovery,
|
|
712
|
-
* gate context) fails closed with no call-site change. `acItems`/`dodItems`
|
|
713
|
-
* stay populated for downstream consumers: from the issue's own checklist
|
|
714
|
-
* sections when present, otherwise projected from the matrix rows.
|
|
715
|
-
*
|
|
716
|
-
* `resolveLinkedDoc` (optional, #1866): a `(path) => boolean` callback used to
|
|
717
|
-
* verify that a linked `tmp/refinement/*.md` doc actually resolves (e.g.
|
|
718
|
-
* `existsSync`). Enforcement-point callers (enqueue gate, draft-gate
|
|
719
|
-
* linked-issue path) supply it; a linked doc found in the body then satisfies
|
|
720
|
-
* the artifact check only when the callback returns true. When the callback is
|
|
721
|
-
* not supplied the predicate stays pure/no-I/O and behavior is unchanged, and
|
|
722
|
-
* the `linkedDoc` result carries no `resolves` field. When supplied and the
|
|
723
|
-
* doc does not resolve, `linkedDoc.resolves === false` and the linked doc does
|
|
724
|
-
* not satisfy the artifact check (other artifact sources still count).
|
|
633
|
+
* The floor is a valid AC→DoD mapping matrix (or a resolvable linked refinement
|
|
634
|
+
* doc) AND an explicit, non-empty Non-goals section. A missing Non-goals
|
|
635
|
+
* section fails closed with the distinct finding
|
|
636
|
+
* `MISSING_EXPLICIT_NON_GOALS_FINDING`; its matcher is shared with
|
|
637
|
+
* `validatePrBodySpec` so the two spec surfaces cannot drift. `hasACs` is true
|
|
638
|
+
* only when the FULL check passes, so every `.hasACs` consumer fails closed with
|
|
639
|
+
* no call-site change. `acItems`/`dodItems` stay populated for downstream
|
|
640
|
+
* consumers: from the issue's own checklist sections when present, otherwise
|
|
641
|
+
* projected from the matrix rows.
|
|
725
642
|
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
643
|
+
* `resolveLinkedDoc` (optional): a `(path) => boolean` callback verifying that a
|
|
644
|
+
* linked `tmp/refinement/*.md` doc actually resolves (e.g. `existsSync`). When
|
|
645
|
+
* not supplied the predicate stays pure/no-I/O and `linkedDoc` carries no
|
|
646
|
+
* `resolves` field; when supplied and the doc does not resolve, the linked doc
|
|
647
|
+
* does not satisfy the check (other artifact sources still count).
|
|
730
648
|
*
|
|
731
649
|
* @param {object} input
|
|
732
650
|
* @param {string} [input.body] Raw issue body Markdown.
|
|
@@ -769,7 +687,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
769
687
|
const acceptanceSection = findSectionByPatterns(sections, ACCEPTANCE_SECTION_PATTERNS);
|
|
770
688
|
const dodSection = findSectionByPatterns(sections, DOD_SECTION_PATTERNS);
|
|
771
689
|
|
|
772
|
-
// CONSUMER-CONTRACT BOUNDARY (
|
|
690
|
+
// CONSUMER-CONTRACT BOUNDARY (intentional asymmetry): the issue-side
|
|
773
691
|
// reads above are strict — ONE exact-first section, NO deep flattening —
|
|
774
692
|
// while extractPrBodyUncheckedChecklistItems (PR side) unions ALL matching
|
|
775
693
|
// sections and deep-flattens past ### sub-headings. The issue side is a
|
|
@@ -784,7 +702,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
784
702
|
const acItems = acceptanceSection ? extractChecklistItems(acceptanceSection.bodyLines.join("\n")) : [];
|
|
785
703
|
// Unticked AC checkboxes (`- [ ]`) of the spec-of-record — the
|
|
786
704
|
// ACCEPT-CRITERIA-VERIFY-AND-REFLECT precondition a clean pre_approval_gate
|
|
787
|
-
// must refuse on
|
|
705
|
+
// must refuse on. Only actual unticked checkboxes count; a ticked
|
|
788
706
|
// box and a plain bullet (no checkbox) are both excluded.
|
|
789
707
|
const uncheckedAcItems = acceptanceSection ? extractUncheckedChecklistItems(acceptanceSection.bodyLines.join("\n")) : [];
|
|
790
708
|
const dodItems = dodSection ? extractChecklistItems(dodSection.bodyLines.join("\n")) : [];
|
|
@@ -796,7 +714,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
796
714
|
linkedDoc = { ...linkedDoc, resolves: linkedDocResolves };
|
|
797
715
|
}
|
|
798
716
|
|
|
799
|
-
//
|
|
717
|
+
// explicit Non-goals section required on a refined tracker-backed
|
|
800
718
|
// issue body — same matcher the PR-body spec path uses, so the two cannot
|
|
801
719
|
// drift. A heading-only or fenced-only section does not count
|
|
802
720
|
// (sectionHasBody anti-spoof).
|
|
@@ -804,7 +722,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
804
722
|
findSectionByPatterns(sections, PR_BODY_SPEC_NARRATIVE_SECTIONS.non_goals.patterns),
|
|
805
723
|
);
|
|
806
724
|
|
|
807
|
-
//
|
|
725
|
+
// the authoritative issue artifact is the AC→DoD mapping MATRIX, not
|
|
808
726
|
// duplicate interactive issue-side checklists. Detect its presence + shape.
|
|
809
727
|
const matrix = detectAcDodMatrix(body);
|
|
810
728
|
|
|
@@ -851,7 +769,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
851
769
|
};
|
|
852
770
|
}
|
|
853
771
|
|
|
854
|
-
// Matrix present but empty/malformed/identifier-only: fail closed
|
|
772
|
+
// Matrix present but empty/malformed/identifier-only: fail closed.
|
|
855
773
|
if (matrix.found && !matrix.valid) {
|
|
856
774
|
return {
|
|
857
775
|
...base,
|
|
@@ -893,8 +811,8 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
893
811
|
}
|
|
894
812
|
|
|
895
813
|
// Matrix absent. If the body carries AC/DoD checklist content it is a
|
|
896
|
-
// checklist-bearing issue missing the authoritative matrix
|
|
897
|
-
// migration
|
|
814
|
+
// checklist-bearing issue missing the authoritative matrix during
|
|
815
|
+
// migration: fail closed on the missing matrix so it is re-grilled. A body
|
|
898
816
|
// with no matrix and no AC/DoD content (prose-only, or only a Non-goals
|
|
899
817
|
// section / an unresolved linked-doc mention) stays the pre-existing
|
|
900
818
|
// missing_refinement_artifact.
|
|
@@ -925,7 +843,7 @@ export function detectIssueRefinementArtifact({ body = "", issueNumber = null, r
|
|
|
925
843
|
}
|
|
926
844
|
|
|
927
845
|
/**
|
|
928
|
-
* PR-body-as-spec invariant sections (
|
|
846
|
+
* PR-body-as-spec invariant sections (lightweight path).
|
|
929
847
|
*
|
|
930
848
|
* When a lightweight session uses the PR description itself as the
|
|
931
849
|
* spec-of-record (no committed phase/plan doc), the PR body must still carry
|
|
@@ -960,9 +878,8 @@ export const PR_BODY_SPEC_NARRATIVE_SECTIONS = Object.freeze({
|
|
|
960
878
|
/**
|
|
961
879
|
* GitHub's accepted closing-keyword issue references (close/closes/closed,
|
|
962
880
|
* fix/fixes/fixed, resolve/resolves/resolved), case-insensitive, followed by
|
|
963
|
-
* `#N` or the cross-repo `owner/repo#N` form.
|
|
964
|
-
* lightweight
|
|
965
|
-
* lightweight PRs merged without this and none auto-closed their issue).
|
|
881
|
+
* `#N` or the cross-repo `owner/repo#N` form. Required linkage on the PR body:
|
|
882
|
+
* five lightweight PRs merged without this and none auto-closed their issue.
|
|
966
883
|
*/
|
|
967
884
|
const CLOSING_ISSUE_REFERENCE_PATTERN =
|
|
968
885
|
/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:[\w.-]+\/[\w.-]+)?#(\d+)/giu;
|
|
@@ -1000,7 +917,7 @@ function extractClosingIssueNumbers(body) {
|
|
|
1000
917
|
function sectionHasBody(section) {
|
|
1001
918
|
// A real body needs >=1 non-whitespace line OUTSIDE any fenced code span —
|
|
1002
919
|
// a section whose only content is a ```fenced``` block is treated as empty so
|
|
1003
|
-
// it cannot spoof the narrative-invariant gate (
|
|
920
|
+
// it cannot spoof the narrative-invariant gate (same stepFence as
|
|
1004
921
|
// parseMarkdownSections + extractChecklistItems).
|
|
1005
922
|
if (!section) return false;
|
|
1006
923
|
let fence = null;
|
|
@@ -1017,40 +934,29 @@ function sectionHasBody(section) {
|
|
|
1017
934
|
* Validate that a PR body carries every invariant required to serve as the
|
|
1018
935
|
* lightweight spec-of-record: Objective/why, in-scope, explicit non-goals,
|
|
1019
936
|
* testable Acceptance criteria (>=1 checklist item), Definition of done
|
|
1020
|
-
* (>=1 checklist item), Open questions/risks, and — unless
|
|
1021
|
-
*
|
|
1022
|
-
*
|
|
1023
|
-
*
|
|
1024
|
-
* (parseMarkdownSections / AC + DoD patterns / extractChecklistItems) so
|
|
1025
|
-
* there is no parallel validator. Fails closed: every missing invariant is
|
|
1026
|
-
* reported under its distinct `missing_*` code. Pure; no side effects.
|
|
937
|
+
* (>=1 checklist item), Open questions/risks, and — unless issue-less mode is
|
|
938
|
+
* requested — a GitHub closing-keyword issue reference. Reuses the generic
|
|
939
|
+
* markdown logic so there is no parallel validator. Fails closed: every missing
|
|
940
|
+
* invariant is reported under its distinct `missing_*` code. Pure; no I/O.
|
|
1027
941
|
*
|
|
1028
|
-
* Issue-less mode (`issueLess: true
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* so callers can tell "no issue expected" apart from "issue expected but
|
|
1035
|
-
* absent". `expectedIssue` and `issueLess` are mutually exclusive; callers
|
|
1036
|
-
* pick exactly one mode (tracker-backed, with or without a specific
|
|
1037
|
-
* expected issue) or issue-less — never both.
|
|
942
|
+
* Issue-less mode (`issueLess: true`): the closing-issue linkage flips from
|
|
943
|
+
* REQUIRED to FORBIDDEN (the PR is the sole artifact), failing closed under
|
|
944
|
+
* `unexpected_closing_issue_reference` — distinct from
|
|
945
|
+
* `missing_closing_issue_reference` (tracker-backed, the default).
|
|
946
|
+
* `expectedIssue` and `issueLess` are mutually exclusive; callers pick exactly
|
|
947
|
+
* one mode.
|
|
1038
948
|
*
|
|
1039
|
-
* `requireOpenQuestions` (default `true
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
* contract") does not name one. Pass `false` (see
|
|
1044
|
-
* `validateTrackerBackedPrBodySpec` below) to skip the `missing_open_questions`
|
|
1045
|
-
* check without touching any other invariant — the lightweight caller's
|
|
1046
|
-
* default stays byte-identical.
|
|
949
|
+
* `requireOpenQuestions` (default `true`): the tracker-backed PR-description
|
|
950
|
+
* contract does not name an Open questions/risks section; pass `false` (see
|
|
951
|
+
* `validateTrackerBackedPrBodySpec`) to skip that check without touching any
|
|
952
|
+
* other invariant.
|
|
1047
953
|
*
|
|
1048
954
|
* @param {{ body?: string, expectedIssue?: number, issueLess?: boolean, requireOpenQuestions?: boolean }} input
|
|
1049
955
|
* @returns {{ checker: "validate-pr-body-spec", ok: boolean, errors: { code: string, message: string }[], sections: string[], acItems: string[], dodItems: string[], closesIssues: number[] }}
|
|
1050
956
|
*/
|
|
1051
957
|
|
|
1052
958
|
// ---------------------------------------------------------------------------
|
|
1053
|
-
// Grill sub-loop body predicates (GRILL-SUBLOOP
|
|
959
|
+
// Grill sub-loop body predicates (GRILL-SUBLOOP-*)
|
|
1054
960
|
// ---------------------------------------------------------------------------
|
|
1055
961
|
// The loop-grill skill writes its raw Q&A transcript and synthesis to an
|
|
1056
962
|
// ephemeral tmp artifact and keeps only the canonical synthesized sections
|
|
@@ -1170,8 +1076,8 @@ export function validatePrBodySpec({ body = "", expectedIssue = null, issueLess
|
|
|
1170
1076
|
|
|
1171
1077
|
/**
|
|
1172
1078
|
* Validate a TRACKER-BACKED PR's own body against the PR-description contract
|
|
1173
|
-
* (skills/docs/copilot-loop-operations.md "PR description contract"
|
|
1174
|
-
*
|
|
1079
|
+
* (skills/docs/copilot-loop-operations.md "PR description contract"):
|
|
1080
|
+
* Acceptance criteria + Definition of done checklists, an explicit
|
|
1175
1081
|
* Non-goals section, and a `Closes #N`/`Fixes #N` reference — regardless of
|
|
1176
1082
|
* whether the linked issue itself already carries a refinement artifact. A
|
|
1177
1083
|
* linked issue with real ACs is necessary but not sufficient: the PR body is
|
|
@@ -1194,7 +1100,7 @@ export function validateTrackerBackedPrBodySpec({ body = "", closingIssues = []
|
|
|
1194
1100
|
}
|
|
1195
1101
|
|
|
1196
1102
|
/**
|
|
1197
|
-
*
|
|
1103
|
+
* Extract the UNCHECKED AC/DoD checkbox items from a PR body's own
|
|
1198
1104
|
* Acceptance criteria / Definition of done checklists — the derived,
|
|
1199
1105
|
* self-contained checklist that mirrors the linked issue's AC/DoD/Non-goals
|
|
1200
1106
|
* matrix. Any unchecked `- [ ]` in those sections means an acceptance
|
|
@@ -1212,7 +1118,7 @@ export function validateTrackerBackedPrBodySpec({ body = "", closingIssues = []
|
|
|
1212
1118
|
* (same `parseMarkdownSections` + `extractUncheckedChecklistItems` seams as
|
|
1213
1119
|
* `detectIssueRefinementArtifact` / `validatePrBodySpec`) so no parallel
|
|
1214
1120
|
* parser can drift. Sections absent from the body contribute no items — the
|
|
1215
|
-
* draft-exit `validateTrackerBackedPrBodySpec` check
|
|
1121
|
+
* draft-exit `validateTrackerBackedPrBodySpec` check already owns
|
|
1216
1122
|
* requiring the sections to EXIST.
|
|
1217
1123
|
*
|
|
1218
1124
|
* @param {{ body?: string }} input
|
|
@@ -1225,7 +1131,7 @@ export function extractPrBodyUncheckedChecklistItems({ body = "" } = {}) {
|
|
|
1225
1131
|
const sections = parseMarkdownSections(body);
|
|
1226
1132
|
// Union the unchecked boxes across ALL sections matching each pattern
|
|
1227
1133
|
// family (exact-first ordered), flattening each section past its deeper
|
|
1228
|
-
// sub-headings
|
|
1134
|
+
// sub-headings: a body nesting ACs under `###` subsections, or
|
|
1229
1135
|
// repeating an AC/DoD heading, must not hide unchecked boxes from the
|
|
1230
1136
|
// deterministic completeness block. Deduped by text (same box re-read in a
|
|
1231
1137
|
// duplicate section is the same box).
|
|
@@ -1268,7 +1174,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1268
1174
|
if (!targetIsPickup || artifact.finding === null) {
|
|
1269
1175
|
return { action: "enqueue" };
|
|
1270
1176
|
}
|
|
1271
|
-
//
|
|
1177
|
+
// artifact present but the contract-mandated Non-goals section is
|
|
1272
1178
|
// absent/empty — a distinct failure with its own guidance.
|
|
1273
1179
|
if (artifact.finding === MISSING_EXPLICIT_NON_GOALS_FINDING) {
|
|
1274
1180
|
const reason =
|
|
@@ -1277,7 +1183,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1277
1183
|
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run `/dev-loops:loop-grill <issue> --auto` (or `/loop-grill <issue> --auto` in the dev-loops repo itself)) — refusing to enqueue without an explicit Non-goals section.";
|
|
1278
1184
|
return { action: auto ? "divert" : "block", reason, missing: ["explicit Non-goals section"] };
|
|
1279
1185
|
}
|
|
1280
|
-
//
|
|
1186
|
+
// matrix present but empty/malformed/identifier-only — name the shape
|
|
1281
1187
|
// defect so the fix targets the mapping table, not a missing section.
|
|
1282
1188
|
if (artifact.finding === MALFORMED_AC_DOD_MATRIX_FINDING) {
|
|
1283
1189
|
const reason =
|
|
@@ -1286,7 +1192,7 @@ export function decideEnqueueRefinementGate({ artifact, targetIsPickup, auto = f
|
|
|
1286
1192
|
"(rule ARTIFACT-TRACKER-ISSUE-REFINEMENT-FLOOR; e.g. run `/dev-loops:loop-grill <issue> --auto` (or `/loop-grill <issue> --auto` in the dev-loops repo itself)) — refusing to enqueue on a malformed matrix.";
|
|
1287
1193
|
return { action: auto ? "divert" : "block", reason, missing: ["valid AC→DoD mapping matrix"] };
|
|
1288
1194
|
}
|
|
1289
|
-
//
|
|
1195
|
+
// matrix absent (whether or not the body carries duplicate issue-side
|
|
1290
1196
|
// checklists) — the mapping table is the authoritative issue artifact.
|
|
1291
1197
|
if (artifact.finding === MISSING_AC_DOD_MATRIX_FINDING) {
|
|
1292
1198
|
const reason =
|
|
@@ -1333,7 +1239,7 @@ export async function runPickupRefinementGate({ issueNumber, repo, env, runChild
|
|
|
1333
1239
|
throw new Error("Invalid JSON input");
|
|
1334
1240
|
}
|
|
1335
1241
|
const body = typeof bodyPayload?.body === "string" ? bodyPayload.body : "";
|
|
1336
|
-
//
|
|
1242
|
+
// a linked refinement doc satisfies the gate only when it actually
|
|
1337
1243
|
// resolves. Paths follow the `tmp/refinement/*.md` convention and are
|
|
1338
1244
|
// anchored to the caller's repo root (`repoRoot` option, falling back to
|
|
1339
1245
|
// process.cwd()) — never the ambient cwd of whichever subdirectory the
|