@dev-loops/core 1.0.0 → 1.0.2-pre.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 +4 -2
- package/src/claude/hook-decisions.mjs +144 -20
- package/src/config/config.mjs +11 -97
- package/src/config/extension-defaults.yaml +18 -7
- package/src/github/comment-id-guard.mjs +38 -0
- package/src/github/gh.mjs +49 -0
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/gate-carry-forward.mjs +45 -21
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +47 -20
- package/src/loop/handoff-envelope.mjs +2 -2
- package/src/loop/issue-refinement-artifact.mjs +608 -75
- package/src/loop/pr-gate-coordination.mjs +27 -2
- package/src/loop/queue-board-sync.mjs +7 -38
- package/src/loop/review-dispatch-plan.mjs +1 -0
- package/src/loop/spec-authority.mjs +759 -0
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +1 -30
- package/src/projects/move-queue-item.mjs +1 -30
- package/src/projects/resolve-project.mjs +6 -6
- package/src/security/secret-scan.mjs +13 -1
|
@@ -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
|
+
MALFORMED_AC_DOD_MATRIX_FINDING,
|
|
8
|
+
MISSING_AC_DOD_MATRIX_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
|
+
// #1951 matrix-floor vocabulary for the draft-gate blocked reason: which piece
|
|
250
|
+
// of the refinement floor is missing per finding — the SAME finding taxonomy
|
|
251
|
+
// the enqueue 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. A checklist-only or
|
|
254
|
+
// matrix-malformed linked issue names the actually-missing/invalid artifact so
|
|
255
|
+
// the fix is not misdirected.
|
|
256
|
+
const REFINEMENT_MISSING_ARM_BY_FINDING = Object.freeze({
|
|
257
|
+
[MISSING_AC_DOD_MATRIX_FINDING]: "an AC→DoD mapping matrix (a two-column table mapping each acceptance-criterion outcome to its required completion evidence)",
|
|
258
|
+
[MALFORMED_AC_DOD_MATRIX_FINDING]: "a valid AC→DoD mapping matrix (the existing table is empty or identifier-only/tautological)",
|
|
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 AC→DoD mapping matrix + Non-goals refinement floor, 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 (
|
|
275
|
+
return `Linked issue #${linkedIssue} has no refinement artifact (no AC→DoD mapping matrix, or a resolvable linked refinement doc). Refine the issue to the authoritative AC→DoD mapping matrix plus an explicit Non-goals section — 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 (
|
|
277
|
+
return `The draft gate cannot complete: the linked issue has no detectable refinement artifact (no AC→DoD mapping matrix, or a 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,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { main as moveQueueItemMain } from "../projects/move-queue-item.mjs";
|
|
5
|
-
import { ghGraphql } from "../github/gh.mjs";
|
|
5
|
+
import { ghGraphql, resolveOwner } from "../github/gh.mjs";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_NON_SUCCESS_COLUMN = "Backlog";
|
|
8
8
|
|
|
@@ -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
|
|
|
@@ -281,18 +277,6 @@ export function loadStateColumnMap(repoRoot) {
|
|
|
281
277
|
|
|
282
278
|
// ── Minimal project lookup (read-only, no create/repair) ────────────────
|
|
283
279
|
|
|
284
|
-
const GET_USER_ID = [
|
|
285
|
-
"query($login:String!) {",
|
|
286
|
-
" user(login:$login) { id }",
|
|
287
|
-
"}"
|
|
288
|
-
].join("\n");
|
|
289
|
-
|
|
290
|
-
const GET_ORG_ID = [
|
|
291
|
-
"query($login:String!) {",
|
|
292
|
-
" organization(login:$login) { id }",
|
|
293
|
-
"}"
|
|
294
|
-
].join("\n");
|
|
295
|
-
|
|
296
280
|
const LIST_USER_PROJECTS = [
|
|
297
281
|
"query($login:String!, $after:String) {",
|
|
298
282
|
" user(login:$login) {",
|
|
@@ -315,21 +299,6 @@ const LIST_ORG_PROJECTS = [
|
|
|
315
299
|
"}"
|
|
316
300
|
].join("\n");
|
|
317
301
|
|
|
318
|
-
async function resolveOwner(login, env, runChild) {
|
|
319
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
320
|
-
if (userPayload?.data?.user?.id) {
|
|
321
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
322
|
-
}
|
|
323
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
324
|
-
if (orgPayload?.data?.organization?.id) {
|
|
325
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
326
|
-
}
|
|
327
|
-
throw Object.assign(
|
|
328
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
329
|
-
{ code: "NO_USER_ID" },
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
302
|
async function listAllProjects(login, kind, env, runChild) {
|
|
334
303
|
const query = kind === "org" ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS;
|
|
335
304
|
const projects = [];
|
|
@@ -869,6 +869,7 @@ export function verifyPromptLeadingAlignment({ promptLeading, prefixBytes, prefi
|
|
|
869
869
|
*/
|
|
870
870
|
export const DEFAULT_DIFF_EXCLUDE_GLOBS = Object.freeze([
|
|
871
871
|
// Lockfiles.
|
|
872
|
+
"bun.lock", "**/bun.lock",
|
|
872
873
|
"package-lock.json", "**/package-lock.json",
|
|
873
874
|
"npm-shrinkwrap.json", "**/npm-shrinkwrap.json",
|
|
874
875
|
"yarn.lock", "**/yarn.lock",
|