@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
|
@@ -39,6 +39,7 @@ export const REGISTERED_ARTIFACT_PATHS = Object.freeze([
|
|
|
39
39
|
"docs/presentations/introducing-dev-loops.html",
|
|
40
40
|
"docs/presentations/dev-loops-deep-dive.html",
|
|
41
41
|
"docs/presentations/how-dev-loops-decided-itself.html",
|
|
42
|
+
"docs/presentations/state-graph-surface.html",
|
|
42
43
|
"docs/articles/introducing-dev-loops.html",
|
|
43
44
|
"docs/articles/dev-loops-deep-dive.html",
|
|
44
45
|
"docs/articles/how-dev-loops-decided-itself.html",
|
|
@@ -192,6 +192,61 @@ export function isWorktreeCoreIsolated(cwd, worktreePaths) {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Realpath-normalize a path that MAY NOT EXIST yet.
|
|
197
|
+
*
|
|
198
|
+
* `realpathSync` throws ENOENT on a nonexistent leaf — which is exactly a `Write`
|
|
199
|
+
* creating a NEW file. Resolving only the roots (worktree/main) while leaving the
|
|
200
|
+
* target un-normalized makes an under-a-symlinked-ancestor comparison asymmetric,
|
|
201
|
+
* so a wrong-checkout new-file write could be misclassified. This resolves the
|
|
202
|
+
* realpath of the target's NEAREST EXISTING ancestor and rejoins the nonexistent
|
|
203
|
+
* tail, so the returned path shares the same symlink-resolved prefix the roots do.
|
|
204
|
+
*
|
|
205
|
+
* @param {string} p - Absolute or relative path (possibly not yet existing).
|
|
206
|
+
* @returns {string} A realpath-normalized absolute path (forward-slash, no trailing slash).
|
|
207
|
+
*/
|
|
208
|
+
export function realpathNearestExisting(p) {
|
|
209
|
+
const abs = path.resolve(p);
|
|
210
|
+
let dir = abs;
|
|
211
|
+
const tail = [];
|
|
212
|
+
// Walk up to the nearest existing ancestor.
|
|
213
|
+
for (;;) {
|
|
214
|
+
try {
|
|
215
|
+
const real = realpathSync(dir);
|
|
216
|
+
const joined = tail.length ? path.join(real, ...tail) : real;
|
|
217
|
+
return joined.replace(/\\/g, "/").replace(/\/+$/u, "");
|
|
218
|
+
} catch {
|
|
219
|
+
const parent = path.dirname(dir);
|
|
220
|
+
if (parent === dir) {
|
|
221
|
+
// Reached the filesystem root without an existing ancestor — fall back
|
|
222
|
+
// to the literal resolved path (nothing to realpath against).
|
|
223
|
+
return abs.replace(/\\/g, "/").replace(/\/+$/u, "");
|
|
224
|
+
}
|
|
225
|
+
tail.unshift(path.basename(dir));
|
|
226
|
+
dir = parent;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Map a `git check-ignore -q` outcome to whether a MAIN-checkout target must be
|
|
233
|
+
* treated as a guarded (non-gitignored) file, failing SAFE on an unresolvable
|
|
234
|
+
* outcome.
|
|
235
|
+
*
|
|
236
|
+
* `git check-ignore -q` exits 0 when the path IS gitignored and 1 when it is NOT.
|
|
237
|
+
* Any other exit (git error, missing binary) is UNRESOLVABLE: the wrong-checkout
|
|
238
|
+
* guard must not silently allow a write it cannot classify, so an unresolvable
|
|
239
|
+
* outcome is treated as guarded (deny) rather than ignored (allow).
|
|
240
|
+
*
|
|
241
|
+
* @param {number|null|undefined} checkIgnoreStatus - The `git check-ignore -q` exit status (null when it could not run).
|
|
242
|
+
* @returns {boolean} true when the target should be guarded (not-ignored, or unresolvable → fail-safe); false only when it is confirmed gitignored (exit 0).
|
|
243
|
+
*/
|
|
244
|
+
export function resolveTrackedFromCheckIgnore(checkIgnoreStatus) {
|
|
245
|
+
if (checkIgnoreStatus === 0) return false; // confirmed gitignored — not guarded
|
|
246
|
+
if (checkIgnoreStatus === 1) return true; // confirmed not-ignored — guarded
|
|
247
|
+
return true; // unresolvable — fail safe (AC4)
|
|
248
|
+
}
|
|
249
|
+
|
|
195
250
|
|
|
196
251
|
// ---------------------------------------------------------------------------
|
|
197
252
|
// Subagent availability
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runChild as _runChild } from "../cli/primitives.mjs";
|
|
2
2
|
import { resolveProjectSelector, findProject } from "./resolve-project.mjs";
|
|
3
|
-
import { ghGraphql } from "../github/gh.mjs";
|
|
3
|
+
import { ghGraphql, resolveOwner } from "../github/gh.mjs";
|
|
4
4
|
|
|
5
5
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
6
6
|
|
|
@@ -32,18 +32,6 @@ function validateRepo(repo) {
|
|
|
32
32
|
|
|
33
33
|
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
34
34
|
|
|
35
|
-
const GET_USER_ID = [
|
|
36
|
-
"query($login:String!) {",
|
|
37
|
-
" user(login:$login) { id }",
|
|
38
|
-
"}"
|
|
39
|
-
].join("\n");
|
|
40
|
-
|
|
41
|
-
const GET_ORG_ID = [
|
|
42
|
-
"query($login:String!) {",
|
|
43
|
-
" organization(login:$login) { id }",
|
|
44
|
-
"}"
|
|
45
|
-
].join("\n");
|
|
46
|
-
|
|
47
35
|
const LIST_USER_PROJECTS = [
|
|
48
36
|
"query($login:String!, $after:String) {",
|
|
49
37
|
" user(login:$login) {",
|
|
@@ -111,23 +99,6 @@ const GET_PROJECT_ITEMS = [
|
|
|
111
99
|
"}"
|
|
112
100
|
].join("\n");
|
|
113
101
|
|
|
114
|
-
// ── Owner resolution ────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
async function resolveOwner(login, env, runChild) {
|
|
117
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
118
|
-
if (userPayload?.data?.user?.id) {
|
|
119
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
120
|
-
}
|
|
121
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
122
|
-
if (orgPayload?.data?.organization?.id) {
|
|
123
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
124
|
-
}
|
|
125
|
-
throw Object.assign(
|
|
126
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
127
|
-
{ code: "NO_USER_ID" },
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
102
|
// ── Paginated project listing ────────────────────────────────────────────
|
|
132
103
|
|
|
133
104
|
async function listAllProjects(login, kind, env, runChild) {
|
|
@@ -2,7 +2,7 @@ import { runChild as _runChild } from "../cli/primitives.mjs";
|
|
|
2
2
|
import { runPickupRefinementGate } from "../loop/issue-refinement-artifact.mjs";
|
|
3
3
|
import { loadStateColumnMap, LOGICAL_COLUMN } from "../loop/queue-board-sync.mjs";
|
|
4
4
|
import { resolveProjectSelector, findProject, parseItemRef } from "./resolve-project.mjs";
|
|
5
|
-
import { ghGraphql } from "../github/gh.mjs";
|
|
5
|
+
import { ghGraphql, resolveOwner } from "../github/gh.mjs";
|
|
6
6
|
|
|
7
7
|
// ── Validation ───────────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -31,18 +31,6 @@ function validateRepo(repo) {
|
|
|
31
31
|
|
|
32
32
|
// ── GraphQL fragments ────────────────────────────────────────────────────
|
|
33
33
|
|
|
34
|
-
const GET_USER_ID = [
|
|
35
|
-
"query($login:String!) {",
|
|
36
|
-
" user(login:$login) { id }",
|
|
37
|
-
"}"
|
|
38
|
-
].join("\n");
|
|
39
|
-
|
|
40
|
-
const GET_ORG_ID = [
|
|
41
|
-
"query($login:String!) {",
|
|
42
|
-
" organization(login:$login) { id }",
|
|
43
|
-
"}"
|
|
44
|
-
].join("\n");
|
|
45
|
-
|
|
46
34
|
const LIST_USER_PROJECTS = [
|
|
47
35
|
"query($login:String!, $after:String) {",
|
|
48
36
|
" user(login:$login) {",
|
|
@@ -120,23 +108,6 @@ const UPDATE_ITEM_FIELD = [
|
|
|
120
108
|
"}"
|
|
121
109
|
].join("\n");
|
|
122
110
|
|
|
123
|
-
// ── Owner resolution ────────────────────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
async function resolveOwner(login, env, runChild) {
|
|
126
|
-
const userPayload = await ghGraphql(GET_USER_ID, { login }, env, runChild);
|
|
127
|
-
if (userPayload?.data?.user?.id) {
|
|
128
|
-
return { id: userPayload.data.user.id, kind: "user" };
|
|
129
|
-
}
|
|
130
|
-
const orgPayload = await ghGraphql(GET_ORG_ID, { login }, env, runChild);
|
|
131
|
-
if (orgPayload?.data?.organization?.id) {
|
|
132
|
-
return { id: orgPayload.data.organization.id, kind: "org" };
|
|
133
|
-
}
|
|
134
|
-
throw Object.assign(
|
|
135
|
-
new Error(`Could not resolve owner ID for "${login}"`),
|
|
136
|
-
{ code: "NO_USER_ID" },
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
111
|
// ── Paginated project listing ────────────────────────────────────────────
|
|
141
112
|
|
|
142
113
|
async function listAllProjects(login, kind, env, runChild) {
|
|
@@ -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);
|
|
@@ -322,7 +322,19 @@ export function parseAddedLines(diffText) {
|
|
|
322
322
|
export function scanDiffText(diffText) {
|
|
323
323
|
const findings = [];
|
|
324
324
|
for (const entry of parseAddedLines(diffText)) {
|
|
325
|
-
|
|
325
|
+
// Bun's generated text lock ends registry dependency tuples with a public
|
|
326
|
+
// Subresource Integrity digest. Ignore only that exact generated field in
|
|
327
|
+
// bun.lock; the same high-entropy value in source, another file, or another
|
|
328
|
+
// position on the lock line remains visible to every detector.
|
|
329
|
+
const text = entry.file === "bun.lock"
|
|
330
|
+
? entry.text
|
|
331
|
+
.replace(/, "sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}"\],?$/u, ", \"<registry-integrity>\"]")
|
|
332
|
+
// Bun expands this dependency's published platform package family in
|
|
333
|
+
// both tuple keys and optional-dependency metadata. Several legitimate
|
|
334
|
+
// names cross the generic entropy threshold despite containing no value.
|
|
335
|
+
.replace(/@mariozechner\/clipboard-[a-z0-9-]+/gu, "<clipboard-platform-package>")
|
|
336
|
+
: entry.text;
|
|
337
|
+
for (const hit of scanLineText(text)) {
|
|
326
338
|
findings.push({ file: entry.file, line: entry.line, ...hit });
|
|
327
339
|
}
|
|
328
340
|
}
|