@dev-loops/core 1.0.1 → 1.0.2-slim.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 +5 -2
- package/src/analysis/diff-analyzer.mjs +85 -137
- package/src/claude/asset-generation.mjs +7 -7
- package/src/claude/hook-decisions.mjs +167 -50
- package/src/config/config.mjs +388 -787
- package/src/config/extension-defaults.yaml +14 -9
- package/src/github/comment-id-guard.mjs +39 -1
- package/src/github/copilot-helpers.mjs +90 -158
- package/src/github/gh.mjs +49 -0
- package/src/loop/bash-command-classify.mjs +34 -49
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/conductor-routing.mjs +15 -23
- package/src/loop/copilot-loop-state.mjs +46 -94
- package/src/loop/gate-carry-forward.mjs +46 -22
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +266 -435
- package/src/loop/handoff-envelope.mjs +21 -21
- package/src/loop/issue-refinement-artifact.mjs +449 -284
- package/src/loop/lifecycle-state.mjs +10 -21
- package/src/loop/pr-gate-coordination.mjs +49 -49
- package/src/loop/queue-board-sync.mjs +16 -82
- package/src/loop/review-dispatch-plan.mjs +61 -122
- package/src/loop/review-lineage.mjs +19 -44
- package/src/loop/spec-authority.mjs +729 -0
- package/src/loop/steering.mjs +16 -68
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +16 -175
- package/src/projects/move-queue-item.mjs +16 -171
- package/src/projects/projects-access.mjs +202 -0
- package/src/security/secret-scan.mjs +13 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Canonical, package-owned Projects V2 read mechanics shared by the core
|
|
2
|
+
// list/move/board-sync operations and the root add/ensure/reorder/archive
|
|
3
|
+
// scripts (issue #2038). This module owns ONLY the read mechanics that were
|
|
4
|
+
// byte-for-byte duplicated across those callers:
|
|
5
|
+
//
|
|
6
|
+
// - strict `owner/name` repository validation (INVALID_REPO)
|
|
7
|
+
// - the identical `projectsV2` discovery query + cursor traversal
|
|
8
|
+
// - a generic connection cursor traversal (each caller supplies its own
|
|
9
|
+
// query projection, page size, and traversal boundary)
|
|
10
|
+
// - the identical single-select `Status` field listing
|
|
11
|
+
// - `Status` option extraction from an item's field values
|
|
12
|
+
//
|
|
13
|
+
// It deliberately does NOT own selection policy, caching, null handling,
|
|
14
|
+
// error presentation, mutation policy, or public result shaping — those stay
|
|
15
|
+
// in each command/domain owner. It composes the already-canonical GitHub
|
|
16
|
+
// transport (`ghGraphql`); owner resolution (`resolveOwner`) stays canonical
|
|
17
|
+
// at each caller. It adds no second transport, client class, query DSL, or
|
|
18
|
+
// pagination framework.
|
|
19
|
+
//
|
|
20
|
+
// It imports neither queue orchestration nor repository-root scripts.
|
|
21
|
+
import { ghGraphql } from "../github/gh.mjs";
|
|
22
|
+
|
|
23
|
+
// ── Repository validation ──────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
26
|
+
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Validate a `--repo` value as exactly `owner/name`. Throws INVALID_REPO on
|
|
30
|
+
* empty/missing input, leading/trailing whitespace, a missing slash, or an
|
|
31
|
+
* owner/name that fails GitHub's slug shape. Returns the input unchanged on
|
|
32
|
+
* success. This is the strict Projects repository acceptance rule — narrower
|
|
33
|
+
* than the general repo-slug parser, which is intentionally not substituted.
|
|
34
|
+
*/
|
|
35
|
+
export function validateProjectsRepo(repo) {
|
|
36
|
+
if (!repo || typeof repo !== "string") {
|
|
37
|
+
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
38
|
+
}
|
|
39
|
+
const trimmed = repo.trim();
|
|
40
|
+
if (trimmed !== repo) {
|
|
41
|
+
throw Object.assign(
|
|
42
|
+
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
43
|
+
{ code: "INVALID_REPO" },
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const slashIdx = repo.indexOf("/");
|
|
47
|
+
if (slashIdx === -1) {
|
|
48
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
49
|
+
}
|
|
50
|
+
const owner = repo.slice(0, slashIdx);
|
|
51
|
+
const name = repo.slice(slashIdx + 1);
|
|
52
|
+
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
53
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
54
|
+
}
|
|
55
|
+
return repo;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Generic connection cursor traversal ────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Walk a GraphQL Relay-style connection to completion, accumulating every
|
|
62
|
+
* page's `nodes` in order. Each caller supplies the query, the base variables
|
|
63
|
+
* (the `$after` cursor is injected only once a page reports one), and a
|
|
64
|
+
* `selectConnection(payload)` selector that returns the `{ nodes, pageInfo }`
|
|
65
|
+
* connection for that query. `entity` names the connection in the malformed
|
|
66
|
+
* -page error only.
|
|
67
|
+
*
|
|
68
|
+
* Preserved boundary semantics (identical to every migrated copy):
|
|
69
|
+
* - pages are concatenated in returned order (position/continuation order)
|
|
70
|
+
* - `after` is set from `pageInfo.endCursor` only when `hasNextPage` is true
|
|
71
|
+
* - a page that reports `hasNextPage` with no `endCursor` fails closed with
|
|
72
|
+
* code GH_API_ERROR (an unpaginatable continuation is never silently
|
|
73
|
+
* truncated)
|
|
74
|
+
*
|
|
75
|
+
* @param {object} opts
|
|
76
|
+
* @param {string} opts.query
|
|
77
|
+
* @param {object} [opts.variables]
|
|
78
|
+
* @param {(payload:any)=>any} opts.selectConnection
|
|
79
|
+
* @param {object} opts.env
|
|
80
|
+
* @param {Function} opts.runChild
|
|
81
|
+
* @param {string} [opts.entity]
|
|
82
|
+
* @returns {Promise<any[]>}
|
|
83
|
+
*/
|
|
84
|
+
export async function paginateNodes({ query, variables = {}, selectConnection, env, runChild, entity = "connection" }) {
|
|
85
|
+
const acc = [];
|
|
86
|
+
let after = null;
|
|
87
|
+
while (true) {
|
|
88
|
+
const vars = { ...variables };
|
|
89
|
+
if (after) vars.after = after;
|
|
90
|
+
const payload = await ghGraphql(query, vars, env, runChild);
|
|
91
|
+
const connection = selectConnection(payload);
|
|
92
|
+
const nodes = connection?.nodes ?? [];
|
|
93
|
+
acc.push(...nodes);
|
|
94
|
+
const pageInfo = connection?.pageInfo ?? {};
|
|
95
|
+
if (!pageInfo.hasNextPage) break;
|
|
96
|
+
if (!pageInfo.endCursor) {
|
|
97
|
+
throw Object.assign(
|
|
98
|
+
new Error(`Invalid ${entity} payload: hasNextPage is true but endCursor is missing`),
|
|
99
|
+
{ code: "GH_API_ERROR" },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
after = pageInfo.endCursor;
|
|
103
|
+
}
|
|
104
|
+
return acc;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Project discovery ──────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
const LIST_USER_PROJECTS = [
|
|
110
|
+
"query($login:String!, $after:String) {",
|
|
111
|
+
" user(login:$login) {",
|
|
112
|
+
" projectsV2(first:50, after:$after) {",
|
|
113
|
+
" pageInfo { hasNextPage endCursor }",
|
|
114
|
+
" nodes { id number title url }",
|
|
115
|
+
" }",
|
|
116
|
+
" }",
|
|
117
|
+
"}",
|
|
118
|
+
].join("\n");
|
|
119
|
+
|
|
120
|
+
const LIST_ORG_PROJECTS = [
|
|
121
|
+
"query($login:String!, $after:String) {",
|
|
122
|
+
" organization(login:$login) {",
|
|
123
|
+
" projectsV2(first:50, after:$after) {",
|
|
124
|
+
" pageInfo { hasNextPage endCursor }",
|
|
125
|
+
" nodes { id number title url }",
|
|
126
|
+
" }",
|
|
127
|
+
" }",
|
|
128
|
+
"}",
|
|
129
|
+
].join("\n");
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Discover every ProjectV2 (id/number/title/url) owned by `login`, resolving
|
|
133
|
+
* the user vs organization query from `kind` ("org" selects the organization
|
|
134
|
+
* root, anything else the user root). Traverses all pages. Returns raw nodes;
|
|
135
|
+
* callers that need null filtering (board-sync) filter the result themselves.
|
|
136
|
+
*/
|
|
137
|
+
export function discoverProjects(login, kind, env, runChild) {
|
|
138
|
+
const isOrg = kind === "org";
|
|
139
|
+
return paginateNodes({
|
|
140
|
+
query: isOrg ? LIST_ORG_PROJECTS : LIST_USER_PROJECTS,
|
|
141
|
+
variables: { login },
|
|
142
|
+
selectConnection: (payload) =>
|
|
143
|
+
isOrg ? payload?.data?.organization?.projectsV2 : payload?.data?.user?.projectsV2,
|
|
144
|
+
env,
|
|
145
|
+
runChild,
|
|
146
|
+
entity: "projects list",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Status field listing ───────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const GET_PROJECT_FIELDS = [
|
|
153
|
+
"query($projectId:ID!, $after:String) {",
|
|
154
|
+
" node(id:$projectId) {",
|
|
155
|
+
" ... on ProjectV2 {",
|
|
156
|
+
" fields(first:50, after:$after) {",
|
|
157
|
+
" pageInfo { hasNextPage endCursor }",
|
|
158
|
+
" nodes {",
|
|
159
|
+
" ... on ProjectV2SingleSelectField {",
|
|
160
|
+
" id name",
|
|
161
|
+
" options { id name }",
|
|
162
|
+
" }",
|
|
163
|
+
" }",
|
|
164
|
+
" }",
|
|
165
|
+
" }",
|
|
166
|
+
" }",
|
|
167
|
+
"}",
|
|
168
|
+
].join("\n");
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* List every single-select field (id/name + `options{id name}`) on a project,
|
|
172
|
+
* traversing all pages. This is the identical field projection used by the
|
|
173
|
+
* list/move/add callers. Operations that need a wider option projection (e.g.
|
|
174
|
+
* ensure-queue-board, which also reads option color/description for repair)
|
|
175
|
+
* call `paginateNodes` with their own field query instead.
|
|
176
|
+
*/
|
|
177
|
+
export function listProjectFields(projectId, env, runChild) {
|
|
178
|
+
return paginateNodes({
|
|
179
|
+
query: GET_PROJECT_FIELDS,
|
|
180
|
+
variables: { projectId },
|
|
181
|
+
selectConnection: (payload) => payload?.data?.node?.fields,
|
|
182
|
+
env,
|
|
183
|
+
runChild,
|
|
184
|
+
entity: "fields",
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Status extraction ──────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Extract an item's current `Status` single-select option name from its
|
|
192
|
+
* `fieldValues`, or null when the item has no Status value. Preserves the
|
|
193
|
+
* exact matching used by every caller: the first field value whose owning
|
|
194
|
+
* single-select field is named "Status".
|
|
195
|
+
*/
|
|
196
|
+
export function extractStatus(node) {
|
|
197
|
+
const fvs = node?.fieldValues?.nodes ?? [];
|
|
198
|
+
for (const fv of fvs) {
|
|
199
|
+
if (fv && fv.field && fv.field.name === "Status") return fv.name;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
@@ -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
|
}
|