@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.
@@ -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 seam) takes priority over
10
- // the deprecated `queue.board` — same precedence as loadBoardConfig in
11
- // ../loop/queue-board-sync.mjs and resolveTrackerBoard in ../config/config.mjs.
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 ?? queue?.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 — or the deprecated queue.board — number / title in .devloops)"),
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 > queue.board.number/queue.board.title. Mutates args.
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
- for (const hit of scanLineText(entry.text)) {
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
  }