@dev-loops/core 1.0.0-rc.7 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/config/config.mjs +11 -97
- package/src/config/extension-defaults.yaml +11 -5
- package/src/loop/commit-msg-guard.mjs +168 -0
- package/src/loop/gate-fanin.mjs +13 -7
- package/src/loop/issue-refinement-artifact.mjs +495 -77
- package/src/loop/pr-gate-coordination.mjs +27 -2
- package/src/loop/public-dev-loop-routing.mjs +4 -0
- package/src/loop/queue-board-sync.mjs +6 -10
- package/src/loop/retrospective-checkpoint.mjs +59 -1
- package/src/projects/move-queue-item.mjs +1 -1
- package/src/projects/resolve-project.mjs +6 -6
|
@@ -3,6 +3,11 @@ import { findBlockingTitleMarkers } from "./pr-title-markers.mjs";
|
|
|
3
3
|
import { evaluateUiE2eScoping } from "./ui-e2e-scoping.mjs";
|
|
4
4
|
import { evaluateUiDesignerReviewScoping } from "./ui-designer-review-scoping.mjs";
|
|
5
5
|
import { trimmedOrNull } from "./normalize.mjs";
|
|
6
|
+
import {
|
|
7
|
+
MISSING_AC_CHECKLIST_FINDING,
|
|
8
|
+
MISSING_DOD_CHECKLIST_FINDING,
|
|
9
|
+
MISSING_EXPLICIT_NON_GOALS_FINDING,
|
|
10
|
+
} from "./issue-refinement-artifact.mjs";
|
|
6
11
|
|
|
7
12
|
export const PR_CHECKPOINT = Object.freeze({
|
|
8
13
|
DRAFT_REVIEW: "draft_review",
|
|
@@ -241,15 +246,35 @@ function normalizeRefinementArtifactStatus(value) {
|
|
|
241
246
|
// their own validation-failure reason from the detector; that reason must
|
|
242
247
|
// replace the "linked issue" wording, which does not apply when the PR is the
|
|
243
248
|
// spec-of-record and no linked issue was ever expected.
|
|
249
|
+
// #1877 full-matrix vocabulary for the draft-gate blocked reason: which
|
|
250
|
+
// matrix arm is missing per finding — the SAME finding taxonomy the enqueue
|
|
251
|
+
// gate's guidance (decideEnqueueRefinementGate) and the detector
|
|
252
|
+
// (detectIssueRefinementArtifact) use, so the draft gate (the unconditional
|
|
253
|
+
// backstop for that floor) cannot drift from it. An AC-only or DoD-only
|
|
254
|
+
// linked issue is a matrix miss, not "no artifact" — the guidance must name
|
|
255
|
+
// the actually-missing arm so the fix is not misdirected.
|
|
256
|
+
const REFINEMENT_MISSING_ARM_BY_FINDING = Object.freeze({
|
|
257
|
+
[MISSING_DOD_CHECKLIST_FINDING]: "a Definition of done checklist (mapped to the acceptance criteria)",
|
|
258
|
+
[MISSING_AC_CHECKLIST_FINDING]: "an Acceptance criteria checklist (for the DoD items to map to)",
|
|
259
|
+
[MISSING_EXPLICIT_NON_GOALS_FINDING]: "an explicit Non-goals section",
|
|
260
|
+
});
|
|
261
|
+
|
|
244
262
|
function formatRefinementBlockedReason(linkedIssue, status, refinementArtifact) {
|
|
245
263
|
const specSource = refinementArtifact?.specSource;
|
|
246
264
|
if (specSource != null && specSource !== REFINEMENT_ARTIFACT_SPEC_SOURCE.LINKED_ISSUE && typeof refinementArtifact?.reason === "string" && refinementArtifact.reason.length > 0) {
|
|
247
265
|
return `The draft gate cannot complete: ${refinementArtifact.reason} finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
248
266
|
}
|
|
267
|
+
const finding = typeof refinementArtifact?.finding === "string" && refinementArtifact.finding.length > 0
|
|
268
|
+
? refinementArtifact.finding
|
|
269
|
+
: REFINEMENT_ARTIFACT_FINDING;
|
|
270
|
+
const missingArm = REFINEMENT_MISSING_ARM_BY_FINDING[finding];
|
|
271
|
+
if (missingArm !== undefined) {
|
|
272
|
+
return `Linked issue #${linkedIssue} has an incomplete refinement matrix — it is missing ${missingArm}. Add it to the issue body to complete the full AC/DoD/Non-goals matrix, then re-open the draft PR. finding=${finding}`;
|
|
273
|
+
}
|
|
249
274
|
if (linkedIssue !== null && Number.isInteger(linkedIssue)) {
|
|
250
|
-
return `Linked issue #${linkedIssue} has no refinement artifact (Acceptance criteria
|
|
275
|
+
return `Linked issue #${linkedIssue} has no refinement artifact (no Acceptance criteria checklist, DoD checklist, or resolvable linked refinement doc). Refine the issue to the full AC/DoD/Non-goals matrix — or link a refinement doc (tmp/refinement/*.md), a complete artifact on its own — then re-open the draft PR. finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
251
276
|
}
|
|
252
|
-
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (Acceptance criteria
|
|
277
|
+
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (no Acceptance criteria checklist, DoD checklist, or resolvable linked refinement doc). finding=${REFINEMENT_ARTIFACT_FINDING}`;
|
|
253
278
|
}
|
|
254
279
|
|
|
255
280
|
// #1472: describes the CI state a round-cap-reached fallback branch actually
|
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
evaluateRetrospectiveGate,
|
|
3
3
|
normalizeRetrospectiveCheckpointState,
|
|
4
4
|
normalizeCheckpointCycleIdentity,
|
|
5
|
+
normalizeRetroProvenance,
|
|
5
6
|
resolveCheckpointStateFromArtifact,
|
|
7
|
+
RETROSPECTIVE_PROVENANCE,
|
|
6
8
|
} from "./retrospective-checkpoint.mjs";
|
|
7
9
|
import {
|
|
8
10
|
EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
|
|
@@ -43,7 +45,9 @@ export * from "./public-dev-loop-routing-contract.mjs";
|
|
|
43
45
|
// package export (see skills/docs/retrospective-checkpoint-contract.md).
|
|
44
46
|
export {
|
|
45
47
|
normalizeCheckpointCycleIdentity,
|
|
48
|
+
normalizeRetroProvenance,
|
|
46
49
|
resolveCheckpointStateFromArtifact,
|
|
50
|
+
RETROSPECTIVE_PROVENANCE,
|
|
47
51
|
};
|
|
48
52
|
|
|
49
53
|
const COPILOT_ISSUE_ASSIGNEE = "copilot-swe-agent";
|
|
@@ -172,9 +172,8 @@ function readDevloopsSettings(repoRoot) {
|
|
|
172
172
|
try {
|
|
173
173
|
const raw = readFileSync(base + ext, "utf8");
|
|
174
174
|
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
175
|
-
// `tracker` (issue #1408, the tracker-agnostic seam) is surfaced
|
|
176
|
-
//
|
|
177
|
-
// the deprecated queue.board without a second file read.
|
|
175
|
+
// `tracker` (issue #1408, the tracker-agnostic seam) is surfaced so
|
|
176
|
+
// loadBoardConfig can read tracker.board directly.
|
|
178
177
|
return { settings: settings?.queue ?? null, tracker: settings?.tracker ?? null };
|
|
179
178
|
} catch (err) {
|
|
180
179
|
if (err?.code === "ENOENT") {
|
|
@@ -204,18 +203,15 @@ function boardSelector(board) {
|
|
|
204
203
|
}
|
|
205
204
|
|
|
206
205
|
export function loadBoardConfig(repoRoot) {
|
|
207
|
-
const {
|
|
206
|
+
const { tracker, error } = readDevloopsSettings(repoRoot);
|
|
208
207
|
if (error) {
|
|
209
208
|
return { enabled: false, reason: `config read/parse error: ${error}` };
|
|
210
209
|
}
|
|
211
|
-
// tracker.board (canonical
|
|
212
|
-
//
|
|
213
|
-
//
|
|
210
|
+
// tracker.board (canonical board key, issue #1408) — see resolveTrackerBoard
|
|
211
|
+
// in ../config/config.mjs for the equivalent resolution against the
|
|
212
|
+
// validated, loaded config.
|
|
214
213
|
const trackerBoard = boardSelector(tracker?.board);
|
|
215
214
|
if (trackerBoard) return trackerBoard;
|
|
216
|
-
if (!queue) return { enabled: false };
|
|
217
|
-
const queueBoard = boardSelector(queue.board);
|
|
218
|
-
if (queueBoard) return queueBoard;
|
|
219
215
|
return { enabled: false };
|
|
220
216
|
}
|
|
221
217
|
|
|
@@ -55,6 +55,54 @@ export const RETROSPECTIVE_QUALIFYING_GATES = Object.freeze([
|
|
|
55
55
|
"issue_intake",
|
|
56
56
|
]);
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Provenance context values for a recorded retrospective (issue #1870).
|
|
60
|
+
*
|
|
61
|
+
* A retrospective MUST be produced by a FRESH-CONTEXT, independent dispatch
|
|
62
|
+
* (analogous to a gate reviewer) seeded with the cycle's full agent/subagent
|
|
63
|
+
* tool-call/action/result record — never written inline by the same working
|
|
64
|
+
* context that did the work. A self-authored retro reflects the working
|
|
65
|
+
* agent's own blind spots back at it; it validates consistency, not
|
|
66
|
+
* conformance, so an inline retro fails the checkpoint.
|
|
67
|
+
*/
|
|
68
|
+
export const RETROSPECTIVE_PROVENANCE = Object.freeze({
|
|
69
|
+
/** Dispatched as a fresh, independent context (the only accepting value). */
|
|
70
|
+
CONTEXT_FRESH: "fresh",
|
|
71
|
+
/** Self-authored by the working context — rejected, fails closed. */
|
|
72
|
+
CONTEXT_INLINE: "inline",
|
|
73
|
+
/** The retro was seeded with the full agent/subagent tool-call record. */
|
|
74
|
+
SEEDED_FROM_RECORD: "agent_tool_call_record",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes a retrospective provenance record from a durable checkpoint
|
|
79
|
+
* artifact. Returns the normalized provenance only when it pins a valid
|
|
80
|
+
* fresh-context pass over the full tool-call record:
|
|
81
|
+
* - `context` must normalize to "fresh" (trimmed, case-insensitive; an
|
|
82
|
+
* "inline"/self-authored retro is rejected — it fails closed, never accepted)
|
|
83
|
+
* - `seededFrom` must be exactly "agent_tool_call_record" (the retro audited
|
|
84
|
+
* the cycle's actual behavior, not a summary)
|
|
85
|
+
* - `recordSource` must be a non-blank string (the transcript/journal path
|
|
86
|
+
* the retro was seeded with)
|
|
87
|
+
*
|
|
88
|
+
* Returns null for anything else — absent, malformed, inline, or partial.
|
|
89
|
+
*
|
|
90
|
+
* @param {unknown} value
|
|
91
|
+
* @returns {{context: "fresh", seededFrom: "agent_tool_call_record", recordSource: string}|null}
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeRetroProvenance(value) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const context = typeof value.context === "string" ? value.context.trim().toLowerCase() : "";
|
|
98
|
+
const seededFrom = typeof value.seededFrom === "string" ? value.seededFrom.trim().toLowerCase() : "";
|
|
99
|
+
const recordSource = typeof value.recordSource === "string" ? value.recordSource.trim() : "";
|
|
100
|
+
if (context !== RETROSPECTIVE_PROVENANCE.CONTEXT_FRESH || seededFrom !== RETROSPECTIVE_PROVENANCE.SEEDED_FROM_RECORD || recordSource.length === 0) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return { context: RETROSPECTIVE_PROVENANCE.CONTEXT_FRESH, seededFrom: RETROSPECTIVE_PROVENANCE.SEEDED_FROM_RECORD, recordSource };
|
|
104
|
+
}
|
|
105
|
+
|
|
58
106
|
/**
|
|
59
107
|
* Normalizes an external retrospective checkpoint-state input to one of the
|
|
60
108
|
* stable RETROSPECTIVE_CHECKPOINT_STATE values. Returns null when the value is
|
|
@@ -149,7 +197,17 @@ export function resolveCheckpointStateFromArtifact(artifact, { hasNewerMergeSinc
|
|
|
149
197
|
return hasNewerMergeSinceCheckpoint ? RETROSPECTIVE_CHECKPOINT_STATE.MISSING : RETROSPECTIVE_CHECKPOINT_STATE.SKIPPED;
|
|
150
198
|
}
|
|
151
199
|
if (rawState === "complete") {
|
|
152
|
-
|
|
200
|
+
if (hasNewerMergeSinceCheckpoint) {
|
|
201
|
+
return RETROSPECTIVE_CHECKPOINT_STATE.MISSING;
|
|
202
|
+
}
|
|
203
|
+
// Fresh-context provenance is mandatory (issue #1870): a `complete` record
|
|
204
|
+
// without provenance that pins a fresh-context pass over the full
|
|
205
|
+
// tool-call record — including legacy inline/self-authored retros — fails
|
|
206
|
+
// closed to MISSING. The old inline self-review path is disallowed.
|
|
207
|
+
if (normalizeRetroProvenance(artifact.provenance) === null) {
|
|
208
|
+
return RETROSPECTIVE_CHECKPOINT_STATE.MISSING;
|
|
209
|
+
}
|
|
210
|
+
return RETROSPECTIVE_CHECKPOINT_STATE.COMPLETE;
|
|
153
211
|
}
|
|
154
212
|
// Malformed/unrecognized durable state — fail closed.
|
|
155
213
|
return RETROSPECTIVE_CHECKPOINT_STATE.MISSING;
|
|
@@ -369,7 +369,7 @@ async function main(args, { env = process.env, runChild, cwd = null } = {}) {
|
|
|
369
369
|
{ code: "CONFIG_ERROR" },
|
|
370
370
|
);
|
|
371
371
|
}
|
|
372
|
-
const decision = await runPickupRefinementGate({ issueNumber, repo, env, runChild: child, auto: false });
|
|
372
|
+
const decision = await runPickupRefinementGate({ issueNumber, repo, env, runChild: child, auto: false, repoRoot: cwd });
|
|
373
373
|
refinement = { refined: decision.action === "enqueue" };
|
|
374
374
|
}
|
|
375
375
|
}
|
|
@@ -6,9 +6,9 @@ import { parse as parseYaml } from "yaml";
|
|
|
6
6
|
// resolution used by ensure-queue-board.mjs. Returns { project }, { title },
|
|
7
7
|
// and/or { olderThanDays } when configured; never throws on a missing/bad file.
|
|
8
8
|
//
|
|
9
|
-
// `tracker.board` (issue #1408, the tracker-agnostic
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// The board resolves from `tracker.board` (issue #1408, the tracker-agnostic
|
|
10
|
+
// seam) — same source as loadBoardConfig in ../loop/queue-board-sync.mjs and
|
|
11
|
+
// resolveTrackerBoard in ../config/config.mjs.
|
|
12
12
|
function resolveSettings(cwd) {
|
|
13
13
|
const basePath = path.join(cwd, ".devloops");
|
|
14
14
|
const extensions = ["", ".yaml", ".yml", ".json"];
|
|
@@ -18,7 +18,7 @@ function resolveSettings(cwd) {
|
|
|
18
18
|
const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
|
|
19
19
|
const queue = settings?.queue;
|
|
20
20
|
const out = {};
|
|
21
|
-
const board = settings?.tracker?.board
|
|
21
|
+
const board = settings?.tracker?.board;
|
|
22
22
|
if (board && typeof board === "object") {
|
|
23
23
|
if (typeof board.number === "number" && Number.isInteger(board.number) && board.number > 0) {
|
|
24
24
|
out.project = board.number;
|
|
@@ -139,7 +139,7 @@ function resolveProjectSelector(args) {
|
|
|
139
139
|
: null;
|
|
140
140
|
if (!projectRef && !projectTitle) {
|
|
141
141
|
throw Object.assign(
|
|
142
|
-
new Error("--project is required (or set tracker.board
|
|
142
|
+
new Error("--project is required (or set tracker.board number / title in .devloops)"),
|
|
143
143
|
{ code: "INVALID_PROJECT" },
|
|
144
144
|
);
|
|
145
145
|
}
|
|
@@ -178,7 +178,7 @@ function findProject(projects, { projectRef, projectTitle }, owner) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Apply .devloops board settings when --project was not passed. Precedence:
|
|
181
|
-
// explicit --project flag >
|
|
181
|
+
// explicit --project flag > tracker.board.number/tracker.board.title. Mutates args.
|
|
182
182
|
function applyDevloopsBoard(args, cwd) {
|
|
183
183
|
if (args.project === undefined) {
|
|
184
184
|
const settings = resolveSettings(cwd);
|