@dev-loops/core 0.1.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/bin/capture-deep-persona-signals.mjs +143 -0
- package/bin/ensure-phase-files.mjs +7 -0
- package/bin/log-bash-exit-1.mjs +7 -0
- package/bin/parse-review-threads.mjs +7 -0
- package/package.json +78 -0
- package/src/analysis/change-classifier.mjs +146 -0
- package/src/analysis/diff-analyzer.mjs +285 -0
- package/src/bash-exit-one.mjs +130 -0
- package/src/cli/helpers.mjs +22 -0
- package/src/cli/primitives.mjs +70 -0
- package/src/cli/retry-wrapper.mjs +169 -0
- package/src/cli/subcommand-runner.mjs +246 -0
- package/src/config/config.mjs +965 -0
- package/src/debt/cluster.mjs +240 -0
- package/src/debt/debt-finding.mjs +68 -0
- package/src/debt/debt-signal.mjs +46 -0
- package/src/debt/deep-persona-signals.mjs +266 -0
- package/src/debt/remediation-to-issue.mjs +121 -0
- package/src/debt/score.mjs +127 -0
- package/src/debt/shape.mjs +214 -0
- package/src/github/copilot-helpers.mjs +343 -0
- package/src/github/repo-slug.mjs +105 -0
- package/src/github/review-threads.mjs +343 -0
- package/src/harness/adapter.mjs +57 -0
- package/src/harness/index.mjs +3 -0
- package/src/harness/noop-adapter.mjs +22 -0
- package/src/harness/pi-adapter.mjs +47 -0
- package/src/loop/async-start-contract.mjs +170 -0
- package/src/loop/conductor-routing.mjs +817 -0
- package/src/loop/copilot-ci-status.mjs +255 -0
- package/src/loop/copilot-loop-iterations.mjs +161 -0
- package/src/loop/copilot-loop-state.mjs +510 -0
- package/src/loop/handoff-envelope.mjs +800 -0
- package/src/loop/issue-refinement-artifact.mjs +268 -0
- package/src/loop/lifecycle-state.mjs +342 -0
- package/src/loop/phase-files.mjs +187 -0
- package/src/loop/policy-constants.mjs +17 -0
- package/src/loop/pr-gate-coordination.mjs +1278 -0
- package/src/loop/public-dev-loop-routing-contract.mjs +277 -0
- package/src/loop/public-dev-loop-routing.mjs +1746 -0
- package/src/loop/queue-board-ordering.mjs +38 -0
- package/src/loop/queue-board-sync.mjs +223 -0
- package/src/loop/queue-driver.mjs +164 -0
- package/src/loop/queue-parallel.mjs +190 -0
- package/src/loop/queue-state.mjs +230 -0
- package/src/loop/retrospective-checkpoint.mjs +178 -0
- package/src/loop/reviewer-loop-state.mjs +456 -0
- package/src/loop/run-inspection.mjs +604 -0
- package/src/loop/steering.mjs +793 -0
- package/src/loop/timeout-policy.mjs +73 -0
- package/src/loop/tracker-first-loop-state.mjs +87 -0
- package/src/loop/tracker-pr-state.mjs +301 -0
- package/src/loop/worktree-guard.mjs +141 -0
- package/src/refinement/ac-dod-matrix.mjs +95 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic issue refinement-artifact detection.
|
|
3
|
+
*
|
|
4
|
+
* Implements the bounded refinement check required by the draft gate per
|
|
5
|
+
* issue #532: a draft PR cannot leave draft unless the linked issue has an
|
|
6
|
+
* explicit refinement artifact (Acceptance criteria section, DoD section,
|
|
7
|
+
* or a linked refinement doc) that the pre-approval gate can verify
|
|
8
|
+
* against. Prose-only issues (Problem / Root Cause / Fix) without an
|
|
9
|
+
* `Acceptance criteria` or `DoD` section cause the draft gate to post
|
|
10
|
+
* `verdict=blocked` with the `missing_refinement_artifact` finding.
|
|
11
|
+
*
|
|
12
|
+
* This module owns:
|
|
13
|
+
* - canonical section-name matching for AC / DoD blocks
|
|
14
|
+
* - bullet-item extraction (both `- [ ]` and `- [x]`)
|
|
15
|
+
* - linked-refinement-doc detection from issue body
|
|
16
|
+
*
|
|
17
|
+
* It deliberately does NOT:
|
|
18
|
+
* - auto-generate ACs from prose
|
|
19
|
+
* - mutate GitHub state
|
|
20
|
+
* - re-implement the issue<->PR linkage detection (callers own that)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const REFINEMENT_SOURCE = Object.freeze({
|
|
24
|
+
ISSUE_BODY_AC: "issue-body-ac",
|
|
25
|
+
ISSUE_BODY_DOD: "issue-body-dod",
|
|
26
|
+
LINKED_DOC: "linked-doc",
|
|
27
|
+
MISSING: "missing",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const REFINEMENT_ARTIFACT_FINDING = "missing_refinement_artifact";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Canonical list of section headings that satisfy the refinement check.
|
|
34
|
+
* Matching is case-insensitive and tolerates trailing/leading whitespace.
|
|
35
|
+
* The two-element minimum keeps the contract explicit:
|
|
36
|
+
* - one AC section (Acceptance criteria)
|
|
37
|
+
* - one DoD-style section (DoD or Definition of Done)
|
|
38
|
+
*/
|
|
39
|
+
const ACCEPTANCE_SECTION_PATTERNS = Object.freeze([
|
|
40
|
+
/^acceptance criteria\s*$/i,
|
|
41
|
+
/^ac\b.*$/i,
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const DOD_SECTION_PATTERNS = Object.freeze([
|
|
45
|
+
/^definition of done\s*$/i,
|
|
46
|
+
/^done\s*$/i,
|
|
47
|
+
/^dod\s*$/i,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract `## ...` heading boundaries from a Markdown body.
|
|
52
|
+
* Returns a sorted array of { level, name, bodyLines } records.
|
|
53
|
+
*/
|
|
54
|
+
export function parseMarkdownSections(body) {
|
|
55
|
+
if (typeof body !== "string" || body.length === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const lines = body.split(/\r?\n/u);
|
|
60
|
+
const sections = [];
|
|
61
|
+
let current = null;
|
|
62
|
+
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
const match = /^(#{1,6})\s+(.+?)\s*$/u.exec(line);
|
|
65
|
+
if (match) {
|
|
66
|
+
if (current) {
|
|
67
|
+
sections.push(current);
|
|
68
|
+
}
|
|
69
|
+
current = {
|
|
70
|
+
level: match[1].length,
|
|
71
|
+
name: match[2],
|
|
72
|
+
bodyLines: [],
|
|
73
|
+
};
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (current) {
|
|
77
|
+
current.bodyLines.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (current) {
|
|
82
|
+
sections.push(current);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return sections;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function findSectionByPatterns(sections, patterns) {
|
|
89
|
+
for (const section of sections) {
|
|
90
|
+
for (const pattern of patterns) {
|
|
91
|
+
if (pattern.test(section.name)) {
|
|
92
|
+
return section;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Extract checklist bullet items (`- [ ]` and `- [x]`) from a section body.
|
|
101
|
+
* Returns the trimmed item text for each matching line. The checkbox state
|
|
102
|
+
* (checked vs unchecked) is intentionally not preserved: callers only need
|
|
103
|
+
* the item text to satisfy the refinement-artifact contract.
|
|
104
|
+
*/
|
|
105
|
+
export function extractChecklistItems(sectionBody) {
|
|
106
|
+
if (typeof sectionBody !== "string" || sectionBody.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const items = [];
|
|
111
|
+
const lines = sectionBody.split(/\r?\n/u);
|
|
112
|
+
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
const match = /^\s*-\s+\[(?:[ xX])\]\s+(.+?)\s*$/u.exec(line);
|
|
115
|
+
if (match) {
|
|
116
|
+
const text = match[1].trim();
|
|
117
|
+
if (text.length > 0) {
|
|
118
|
+
items.push(text);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return items;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detect a linked refinement doc path from the issue body.
|
|
128
|
+
* Looks for explicit `tmp/refinement/<n>-plan.md` style paths and the
|
|
129
|
+
* `## Refinement` / `## Plan` / `## Refinement doc` sections.
|
|
130
|
+
*/
|
|
131
|
+
export function detectLinkedRefinementDoc(body) {
|
|
132
|
+
if (typeof body !== "string" || body.length === 0) {
|
|
133
|
+
return { found: false, path: null, reason: "empty-body" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const pathMatch = /(?:^|\s|[`(\[<])(tmp\/refinement\/[A-Za-z0-9._/\-]+\.md)\b/u.exec(body);
|
|
137
|
+
if (pathMatch) {
|
|
138
|
+
return { found: true, path: pathMatch[1], reason: "explicit-path" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const sections = parseMarkdownSections(body);
|
|
142
|
+
const refinementSection = findSectionByPatterns(sections, [
|
|
143
|
+
/^refinement doc\s*$/i,
|
|
144
|
+
/^refinement\s*$/i,
|
|
145
|
+
/^plan doc\s*$/i,
|
|
146
|
+
/^plan\s*$/i,
|
|
147
|
+
]);
|
|
148
|
+
if (refinementSection) {
|
|
149
|
+
const inlinePath = /(?:^|\s)(tmp\/refinement\/[^\s)`'"]+\.md)\b/u.exec(refinementSection.bodyLines.join("\n"));
|
|
150
|
+
if (inlinePath) {
|
|
151
|
+
return { found: true, path: inlinePath[1], reason: "refinement-section-path" };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { found: false, path: null, reason: "no-linked-doc" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Detect the refinement artifact on a parsed issue body.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} input
|
|
162
|
+
* @param {string} [input.body] Raw issue body Markdown.
|
|
163
|
+
* @param {number} [input.issueNumber] Issue number, used for linked-doc convention.
|
|
164
|
+
* @returns {{
|
|
165
|
+
* hasACs: boolean,
|
|
166
|
+
* source: string,
|
|
167
|
+
* acItems: string[],
|
|
168
|
+
* dodItems: string[],
|
|
169
|
+
* sections: string[],
|
|
170
|
+
* linkedDoc: { found: boolean, path: string|null, reason: string },
|
|
171
|
+
* reason: string,
|
|
172
|
+
* finding: string|null,
|
|
173
|
+
* }}
|
|
174
|
+
*/
|
|
175
|
+
export function detectIssueRefinementArtifact({ body = "", issueNumber = null } = {}) {
|
|
176
|
+
if (typeof body !== "string" || body.length === 0) {
|
|
177
|
+
return {
|
|
178
|
+
hasACs: false,
|
|
179
|
+
source: REFINEMENT_SOURCE.MISSING,
|
|
180
|
+
acItems: [],
|
|
181
|
+
dodItems: [],
|
|
182
|
+
sections: [],
|
|
183
|
+
linkedDoc: { found: false, path: null, reason: "empty-body" },
|
|
184
|
+
reason: "Issue body is empty; no ACs/DoD/linked-doc can be detected.",
|
|
185
|
+
finding: REFINEMENT_ARTIFACT_FINDING,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sections = parseMarkdownSections(body);
|
|
190
|
+
const sectionNames = sections.map((s) => s.name);
|
|
191
|
+
|
|
192
|
+
const acceptanceSection = findSectionByPatterns(sections, ACCEPTANCE_SECTION_PATTERNS);
|
|
193
|
+
const dodSection = findSectionByPatterns(sections, DOD_SECTION_PATTERNS);
|
|
194
|
+
|
|
195
|
+
const acItems = acceptanceSection ? extractChecklistItems(acceptanceSection.bodyLines.join("\n")) : [];
|
|
196
|
+
const dodItems = dodSection ? extractChecklistItems(dodSection.bodyLines.join("\n")) : [];
|
|
197
|
+
|
|
198
|
+
const linkedDoc = detectLinkedRefinementDoc(body);
|
|
199
|
+
|
|
200
|
+
if (acItems.length > 0) {
|
|
201
|
+
return {
|
|
202
|
+
hasACs: true,
|
|
203
|
+
source: REFINEMENT_SOURCE.ISSUE_BODY_AC,
|
|
204
|
+
acItems,
|
|
205
|
+
dodItems,
|
|
206
|
+
sections: sectionNames,
|
|
207
|
+
linkedDoc,
|
|
208
|
+
reason: `Found ${acItems.length} Acceptance criteria checklist item(s) in the issue body.`,
|
|
209
|
+
finding: null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (dodItems.length > 0) {
|
|
214
|
+
return {
|
|
215
|
+
hasACs: true,
|
|
216
|
+
source: REFINEMENT_SOURCE.ISSUE_BODY_DOD,
|
|
217
|
+
acItems,
|
|
218
|
+
dodItems,
|
|
219
|
+
sections: sectionNames,
|
|
220
|
+
linkedDoc,
|
|
221
|
+
reason: `Found ${dodItems.length} DoD checklist item(s) in the issue body.`,
|
|
222
|
+
finding: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (linkedDoc.found) {
|
|
227
|
+
return {
|
|
228
|
+
hasACs: true,
|
|
229
|
+
source: REFINEMENT_SOURCE.LINKED_DOC,
|
|
230
|
+
acItems: [],
|
|
231
|
+
dodItems: [],
|
|
232
|
+
sections: sectionNames,
|
|
233
|
+
linkedDoc,
|
|
234
|
+
reason: `Issue body links a refinement doc at ${linkedDoc.path}; treating that as the refinement artifact source.`,
|
|
235
|
+
finding: null,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
hasACs: false,
|
|
241
|
+
source: REFINEMENT_SOURCE.MISSING,
|
|
242
|
+
acItems: [],
|
|
243
|
+
dodItems: [],
|
|
244
|
+
sections: sectionNames,
|
|
245
|
+
linkedDoc,
|
|
246
|
+
reason: "Issue body has no Acceptance criteria section, no DoD section, and no linked refinement doc.",
|
|
247
|
+
finding: REFINEMENT_ARTIFACT_FINDING,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Map a draft-gate refinement check to the result surface consumed by
|
|
253
|
+
* `evaluatePrGateCoordination`. The mapping keeps the contract
|
|
254
|
+
* deterministic: the draft gate must not produce a `clean` verdict
|
|
255
|
+
* for the current head when the refinement check is `missing`.
|
|
256
|
+
*/
|
|
257
|
+
export function summarizeRefinementGateCheck({ body = "", issueNumber = null } = {}) {
|
|
258
|
+
const artifact = detectIssueRefinementArtifact({ body, issueNumber });
|
|
259
|
+
const verdict = artifact.hasACs ? "clean" : "blocked";
|
|
260
|
+
const finding = artifact.finding;
|
|
261
|
+
return {
|
|
262
|
+
artifact,
|
|
263
|
+
verdict,
|
|
264
|
+
finding,
|
|
265
|
+
blocking: !artifact.hasACs,
|
|
266
|
+
reason: artifact.reason,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic outer dev-loop lifecycle state model.
|
|
3
|
+
*
|
|
4
|
+
* This module defines the sequential lifecycle phases — issue_intake →
|
|
5
|
+
* refinement → implementation → draft_gate → feedback_resolution →
|
|
6
|
+
* pre_approval_gate → merge — as a consultable graph so skills use
|
|
7
|
+
* machine-resolved state instead of restating the flow in prose.
|
|
8
|
+
*
|
|
9
|
+
* This module provides:
|
|
10
|
+
* - LIFECYCLE_STATE: stable phase name constants
|
|
11
|
+
* - LIFECYCLE_TRANSITIONS: legal transition graph between phases
|
|
12
|
+
* - LIFECYCLE_GRAPH: metadata (start, end, entry, terminal, nonterminal)
|
|
13
|
+
* - LIFECYCLE_NEXT_ACTIONS: recommended next action for each phase
|
|
14
|
+
* - resolveLifecycleState: resolver that maps inputs to one lifecycle phase
|
|
15
|
+
* - getAllowedTransitions: helper to list allowed next phases
|
|
16
|
+
* - COPILOT_INNER_STATE_MAP: maps lifecycle phases to copilot-loop-state.mjs inner states
|
|
17
|
+
*
|
|
18
|
+
* Contract guarantees:
|
|
19
|
+
* - One deterministic lifecycle phase per normalized input set
|
|
20
|
+
* - Ambiguous or incomplete inputs fall back to issue_intake
|
|
21
|
+
* - Transition graph enforces legal phase progression
|
|
22
|
+
* - Purely functional; no I/O or side effects
|
|
23
|
+
*
|
|
24
|
+
* Integration boundary:
|
|
25
|
+
* - Skills call resolveLifecycleState to determine current phase
|
|
26
|
+
* - Copilot-loop-state.mjs remains the inner machine for the Copilot review portion
|
|
27
|
+
* - Lifecycle phases are the outer sequence; inner states are sub-phase detail
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Exported constants
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stable lifecycle phase name constants.
|
|
36
|
+
*
|
|
37
|
+
* These are the sequential outer dev-loop phases from issue intake to merge.
|
|
38
|
+
*/
|
|
39
|
+
export const LIFECYCLE_STATE = Object.freeze({
|
|
40
|
+
/** Issue normalization, scope confirmation, PR linkage detection. */
|
|
41
|
+
ISSUE_INTAKE: "issue_intake",
|
|
42
|
+
/** Issue refinement: spec elaboration, audit, acceptance criteria hardening. */
|
|
43
|
+
REFINEMENT: "refinement",
|
|
44
|
+
/** Active code implementation (local or Copilot-assisted). */
|
|
45
|
+
IMPLEMENTATION: "implementation",
|
|
46
|
+
/** Draft gate review before marking PR ready for review. */
|
|
47
|
+
DRAFT_GATE: "draft_gate",
|
|
48
|
+
/** Review feedback fix/reply/resolve loop. */
|
|
49
|
+
FEEDBACK_RESOLUTION: "feedback_resolution",
|
|
50
|
+
/** Pre-approval gate before merge. */
|
|
51
|
+
PRE_APPROVAL_GATE: "pre_approval_gate",
|
|
52
|
+
/** Final merge step. */
|
|
53
|
+
MERGE: "merge",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const LIFECYCLE_STATE_VALUES = Object.freeze(Object.values(LIFECYCLE_STATE));
|
|
57
|
+
const LIFECYCLE_STATE_SET = new Set(LIFECYCLE_STATE_VALUES);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Legal transitions between lifecycle phases.
|
|
61
|
+
*
|
|
62
|
+
* Each entry lists the phases reachable from the given phase.
|
|
63
|
+
* Terminal states (merge) have no outgoing transitions.
|
|
64
|
+
*/
|
|
65
|
+
export const LIFECYCLE_TRANSITIONS = Object.freeze({
|
|
66
|
+
[LIFECYCLE_STATE.ISSUE_INTAKE]: Object.freeze([
|
|
67
|
+
LIFECYCLE_STATE.REFINEMENT,
|
|
68
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
69
|
+
]),
|
|
70
|
+
[LIFECYCLE_STATE.REFINEMENT]: Object.freeze([
|
|
71
|
+
LIFECYCLE_STATE.ISSUE_INTAKE,
|
|
72
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
73
|
+
]),
|
|
74
|
+
[LIFECYCLE_STATE.IMPLEMENTATION]: Object.freeze([
|
|
75
|
+
LIFECYCLE_STATE.DRAFT_GATE,
|
|
76
|
+
LIFECYCLE_STATE.FEEDBACK_RESOLUTION,
|
|
77
|
+
]),
|
|
78
|
+
[LIFECYCLE_STATE.DRAFT_GATE]: Object.freeze([
|
|
79
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
80
|
+
LIFECYCLE_STATE.FEEDBACK_RESOLUTION,
|
|
81
|
+
]),
|
|
82
|
+
[LIFECYCLE_STATE.FEEDBACK_RESOLUTION]: Object.freeze([
|
|
83
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
84
|
+
LIFECYCLE_STATE.PRE_APPROVAL_GATE,
|
|
85
|
+
]),
|
|
86
|
+
[LIFECYCLE_STATE.PRE_APPROVAL_GATE]: Object.freeze([
|
|
87
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
88
|
+
LIFECYCLE_STATE.FEEDBACK_RESOLUTION,
|
|
89
|
+
LIFECYCLE_STATE.MERGE,
|
|
90
|
+
]),
|
|
91
|
+
[LIFECYCLE_STATE.MERGE]: Object.freeze([]),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Terminal lifecycle phases — no further progression. */
|
|
95
|
+
export const LIFECYCLE_TERMINAL_STATES = Object.freeze([
|
|
96
|
+
LIFECYCLE_STATE.MERGE,
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
/** Nonterminal lifecycle phases — progression still possible. */
|
|
100
|
+
export const LIFECYCLE_NONTERMINAL_STATES = Object.freeze([
|
|
101
|
+
LIFECYCLE_STATE.ISSUE_INTAKE,
|
|
102
|
+
LIFECYCLE_STATE.REFINEMENT,
|
|
103
|
+
LIFECYCLE_STATE.IMPLEMENTATION,
|
|
104
|
+
LIFECYCLE_STATE.DRAFT_GATE,
|
|
105
|
+
LIFECYCLE_STATE.FEEDBACK_RESOLUTION,
|
|
106
|
+
LIFECYCLE_STATE.PRE_APPROVAL_GATE,
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const LIFECYCLE_TERMINAL_SET = new Set(LIFECYCLE_TERMINAL_STATES);
|
|
110
|
+
|
|
111
|
+
/** High-level graph metadata for visualization and inspection. */
|
|
112
|
+
export const LIFECYCLE_GRAPH = Object.freeze({
|
|
113
|
+
start: Object.freeze({ id: "lifecycle_start", label: "Start", semantic: true }),
|
|
114
|
+
end: Object.freeze({ id: "lifecycle_end", label: "End", semantic: true }),
|
|
115
|
+
entryState: LIFECYCLE_STATE.ISSUE_INTAKE,
|
|
116
|
+
entryStates: Object.freeze([...LIFECYCLE_STATE_VALUES]),
|
|
117
|
+
terminalStates: LIFECYCLE_TERMINAL_STATES,
|
|
118
|
+
nonterminalStates: LIFECYCLE_NONTERMINAL_STATES,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** Recommended next action for each lifecycle phase. */
|
|
122
|
+
export const LIFECYCLE_NEXT_ACTIONS = Object.freeze({
|
|
123
|
+
[LIFECYCLE_STATE.ISSUE_INTAKE]:
|
|
124
|
+
"Normalize the issue: confirm scope, detect linked PR, and determine readiness.",
|
|
125
|
+
[LIFECYCLE_STATE.REFINEMENT]:
|
|
126
|
+
"Refine the issue: elaborate spec, run bounded audit if needed, harden acceptance criteria.",
|
|
127
|
+
[LIFECYCLE_STATE.IMPLEMENTATION]:
|
|
128
|
+
"Implement the accepted scope on a feature branch or via Copilot handoff.",
|
|
129
|
+
[LIFECYCLE_STATE.DRAFT_GATE]:
|
|
130
|
+
"Run draft gate review before marking the PR ready for review.",
|
|
131
|
+
[LIFECYCLE_STATE.FEEDBACK_RESOLUTION]:
|
|
132
|
+
"Address review feedback: fix, reply to, and resolve threads on GitHub.",
|
|
133
|
+
[LIFECYCLE_STATE.PRE_APPROVAL_GATE]:
|
|
134
|
+
"Run pre-approval gate review; verify gate evidence, CI, and unresolved threads.",
|
|
135
|
+
[LIFECYCLE_STATE.MERGE]:
|
|
136
|
+
"Merge is authorized; run the final merge step and write the retrospective checkpoint.",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Resolver
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Map an explicit lifecycle phase string to its canonical state value.
|
|
145
|
+
* Returns the canonical state if recognized, otherwise null.
|
|
146
|
+
*/
|
|
147
|
+
function normalizeLifecycleState(value) {
|
|
148
|
+
if (typeof value !== "string") return null;
|
|
149
|
+
const trimmed = value.trim().toLowerCase();
|
|
150
|
+
return LIFECYCLE_STATE_SET.has(trimmed) ? trimmed : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the current lifecycle phase from authoritative inputs.
|
|
155
|
+
*
|
|
156
|
+
* Input shape:
|
|
157
|
+
* ```js
|
|
158
|
+
* {
|
|
159
|
+
* phase, // explicit phase string (overrides infer)
|
|
160
|
+
* hasLinkedPr, // boolean: open linked PR exists
|
|
161
|
+
* prIsDraft, // boolean: PR is in draft state
|
|
162
|
+
* hasUnresolvedThreads, // boolean: unresolved review threads exist
|
|
163
|
+
* preApprovalGatePassed, // boolean: current-head pre_approval_gate clean
|
|
164
|
+
* mergeAuthorized, // boolean: explicit merge authorization granted
|
|
165
|
+
* isMerged, // boolean: PR has been merged
|
|
166
|
+
* }
|
|
167
|
+
* ```
|
|
168
|
+
*
|
|
169
|
+
* Returns:
|
|
170
|
+
* ```js
|
|
171
|
+
* {
|
|
172
|
+
* state: string, // canonical lifecycle phase
|
|
173
|
+
* allowedTransitions: string[], // legal next phases
|
|
174
|
+
* nextAction: string, // recommended next action
|
|
175
|
+
* isTerminal: boolean, // true if merge (no further progression)
|
|
176
|
+
* }
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* Resolution order (first-match):
|
|
180
|
+
* 1. Explicit phase → return canonical if recognized, fall through if not
|
|
181
|
+
* 2. Merged → merge (terminal)
|
|
182
|
+
* 3. Merge authorized + pre-approval passed + linked PR → merge
|
|
183
|
+
* 4. Pre-approval passed + PR exists → pre_approval_gate
|
|
184
|
+
* 5. Unresolved threads + PR exists → feedback_resolution
|
|
185
|
+
* 6. Draft PR → implementation
|
|
186
|
+
* 7. Ready PR (not draft) → implementation
|
|
187
|
+
* 8. No linked PR → issue_intake
|
|
188
|
+
*/
|
|
189
|
+
export function resolveLifecycleState(input = {}) {
|
|
190
|
+
const {
|
|
191
|
+
phase = null,
|
|
192
|
+
hasLinkedPr = false,
|
|
193
|
+
prIsDraft = false,
|
|
194
|
+
hasUnresolvedThreads = false,
|
|
195
|
+
preApprovalGatePassed = false,
|
|
196
|
+
mergeAuthorized = false,
|
|
197
|
+
isMerged = false,
|
|
198
|
+
} = input;
|
|
199
|
+
|
|
200
|
+
// 1. Explicit phase override — canonical or fail closed
|
|
201
|
+
if (phase !== null && phase !== undefined) {
|
|
202
|
+
const normalized = normalizeLifecycleState(phase);
|
|
203
|
+
if (normalized) {
|
|
204
|
+
return buildResult(normalized);
|
|
205
|
+
}
|
|
206
|
+
// unrecognized phase: fall through to inference
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Merged → terminal
|
|
210
|
+
if (isMerged) {
|
|
211
|
+
return buildResult(LIFECYCLE_STATE.MERGE);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Merge authorized with pre-approval + linked PR → merge
|
|
215
|
+
if (mergeAuthorized && preApprovalGatePassed && hasLinkedPr) {
|
|
216
|
+
return buildResult(LIFECYCLE_STATE.MERGE);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 4. Pre-approval gate passed (but merge not yet authorized)
|
|
220
|
+
if (preApprovalGatePassed && hasLinkedPr) {
|
|
221
|
+
return buildResult(LIFECYCLE_STATE.PRE_APPROVAL_GATE);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 5. Unresolved threads exist → feedback resolution
|
|
225
|
+
if (hasUnresolvedThreads && hasLinkedPr) {
|
|
226
|
+
return buildResult(LIFECYCLE_STATE.FEEDBACK_RESOLUTION);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6. Draft PR → implementation
|
|
230
|
+
if (prIsDraft && hasLinkedPr) {
|
|
231
|
+
return buildResult(LIFECYCLE_STATE.IMPLEMENTATION);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 7. PR exists (not draft) → implementation
|
|
235
|
+
if (hasLinkedPr && !prIsDraft) {
|
|
236
|
+
return buildResult(LIFECYCLE_STATE.IMPLEMENTATION);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 8. No linked PR → issue intake
|
|
240
|
+
return buildResult(LIFECYCLE_STATE.ISSUE_INTAKE);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildResult(state) {
|
|
244
|
+
const transitions = LIFECYCLE_TRANSITIONS[state] ?? [];
|
|
245
|
+
return {
|
|
246
|
+
state,
|
|
247
|
+
allowedTransitions: [...transitions],
|
|
248
|
+
nextAction: LIFECYCLE_NEXT_ACTIONS[state] ?? "",
|
|
249
|
+
isTerminal: LIFECYCLE_TERMINAL_SET.has(state),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Transition helpers
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Return the allowed next phases for a given lifecycle state.
|
|
259
|
+
* Unknown states return an empty array.
|
|
260
|
+
*/
|
|
261
|
+
export function getAllowedTransitions(state) {
|
|
262
|
+
if (!LIFECYCLE_STATE_SET.has(state)) return [];
|
|
263
|
+
return [...(LIFECYCLE_TRANSITIONS[state] ?? [])];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check whether a transition from one phase to another is legal.
|
|
268
|
+
*/
|
|
269
|
+
export function isTransitionAllowed(fromState, toState) {
|
|
270
|
+
if (!LIFECYCLE_STATE_SET.has(fromState) || !LIFECYCLE_STATE_SET.has(toState)) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
return LIFECYCLE_TRANSITIONS[fromState].includes(toState);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check whether a value is a recognized lifecycle state.
|
|
278
|
+
*/
|
|
279
|
+
export function isKnownLifecycleState(value) {
|
|
280
|
+
return LIFECYCLE_STATE_SET.has(value);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
// Connection to copilot-loop-state.mjs inner machine
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Map from outer lifecycle phases to copilot-loop-state.mjs inner machine states.
|
|
289
|
+
*
|
|
290
|
+
* Skills use this mapping to determine which inner-machine states are active
|
|
291
|
+
* during a given lifecycle phase. Not all lifecycle phases have a corresponding
|
|
292
|
+
* inner-machine state (issue_intake and refinement are outer-only).
|
|
293
|
+
*
|
|
294
|
+
* The inner machine is the authority for Copilot review/fix loop states;
|
|
295
|
+
* this mapping is advisory for routing and status reporting.
|
|
296
|
+
*/
|
|
297
|
+
export const COPILOT_INNER_STATE_MAP = Object.freeze({
|
|
298
|
+
[LIFECYCLE_STATE.ISSUE_INTAKE]: Object.freeze([]),
|
|
299
|
+
[LIFECYCLE_STATE.REFINEMENT]: Object.freeze([]),
|
|
300
|
+
[LIFECYCLE_STATE.IMPLEMENTATION]: Object.freeze([
|
|
301
|
+
"no_pr",
|
|
302
|
+
"pr_draft",
|
|
303
|
+
]),
|
|
304
|
+
[LIFECYCLE_STATE.DRAFT_GATE]: Object.freeze([
|
|
305
|
+
"pr_ready_no_feedback",
|
|
306
|
+
]),
|
|
307
|
+
[LIFECYCLE_STATE.FEEDBACK_RESOLUTION]: Object.freeze([
|
|
308
|
+
"waiting_for_copilot_review",
|
|
309
|
+
"unresolved_feedback_present",
|
|
310
|
+
"already_fixed_needs_reply_resolve",
|
|
311
|
+
"ready_to_rerequest_review",
|
|
312
|
+
"waiting_for_ci",
|
|
313
|
+
"review_request_unavailable",
|
|
314
|
+
"blocked_needs_user_decision",
|
|
315
|
+
"round_cap_reached",
|
|
316
|
+
]),
|
|
317
|
+
[LIFECYCLE_STATE.PRE_APPROVAL_GATE]: Object.freeze([
|
|
318
|
+
"low_signal_converged",
|
|
319
|
+
"round_cap_clean_fallback",
|
|
320
|
+
"internal_tooling_direct_gate",
|
|
321
|
+
]),
|
|
322
|
+
[LIFECYCLE_STATE.MERGE]: Object.freeze([
|
|
323
|
+
"done",
|
|
324
|
+
]),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Resolve the lifecycle phase implied by a given copilot-loop-state.mjs inner state.
|
|
329
|
+
*
|
|
330
|
+
* Returns the lifecycle phase that typically contains the given inner state,
|
|
331
|
+
* or null if the inner state maps to no lifecycle phase.
|
|
332
|
+
*/
|
|
333
|
+
export function lifecyclePhaseForCopilotState(copilotState) {
|
|
334
|
+
if (typeof copilotState !== "string") return null;
|
|
335
|
+
|
|
336
|
+
for (const [phase, innerStates] of Object.entries(COPILOT_INNER_STATE_MAP)) {
|
|
337
|
+
if (innerStates.includes(copilotState)) {
|
|
338
|
+
return phase;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|