@dev-loops/core 1.0.0-rc.2 → 1.0.0-rc.4
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 +7 -1
- package/src/analysis/diff-analyzer.mjs +31 -5
- package/src/claude/hook-decisions.mjs +14 -0
- package/src/cli/primitives.mjs +10 -2
- package/src/cli/retry-wrapper.mjs +14 -6
- package/src/config/config.mjs +1125 -240
- package/src/config/extension-defaults.yaml +217 -426
- package/src/github/copilot-helpers.mjs +139 -18
- package/src/github/issue-ops.mjs +556 -0
- package/src/github/ownership-helpers.mjs +79 -0
- package/src/github/review-threads.mjs +44 -3
- package/src/loop/bash-command-classify.mjs +35 -5
- package/src/loop/conductor-routing.mjs +1 -1
- package/src/loop/copilot-ci-status.mjs +76 -0
- package/src/loop/copilot-loop-iterations.mjs +1 -2
- package/src/loop/copilot-loop-state.mjs +9 -5
- package/src/loop/default-branch-guard.mjs +380 -0
- package/src/loop/gate-carry-forward.mjs +29 -2
- package/src/loop/gate-fanin.mjs +481 -31
- package/src/loop/handoff-envelope.mjs +43 -23
- package/src/loop/main-checkout-ff.mjs +58 -0
- package/src/loop/pr-gate-coordination.mjs +204 -47
- package/src/loop/pr-title-markers.mjs +76 -15
- package/src/loop/queue-board-sync.mjs +26 -9
- package/src/loop/reviewer-loop-state.mjs +2 -2
- package/src/loop/ui-e2e-scoping.mjs +2 -0
- package/src/loop/ui-review-drive.mjs +23 -0
- package/src/loop/ui-review-provision.mjs +36 -0
- package/src/projects/resolve-project.mjs +14 -7
- package/src/tracker/adapter.mjs +127 -0
- package/src/tracker/github-adapter.mjs +150 -0
- package/src/tracker/index.mjs +50 -0
- package/src/tracker/noop-adapter.mjs +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-loops/core",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24"
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"./debt/shape": "./src/debt/shape.mjs",
|
|
22
22
|
"./debt/signal": "./src/debt/debt-signal.mjs",
|
|
23
23
|
"./github/copilot-helpers": "./src/github/copilot-helpers.mjs",
|
|
24
|
+
"./github/issue-ops": "./src/github/issue-ops.mjs",
|
|
25
|
+
"./github/ownership-helpers": "./src/github/ownership-helpers.mjs",
|
|
24
26
|
"./github/repo-slug": "./src/github/repo-slug.mjs",
|
|
25
27
|
"./github/review-threads": "./src/github/review-threads.mjs",
|
|
26
28
|
"./loop/async-start-contract": "./src/loop/async-start-contract.mjs",
|
|
@@ -43,8 +45,10 @@
|
|
|
43
45
|
"./loop/pr-lifecycle": "./src/loop/pr-lifecycle.mjs",
|
|
44
46
|
"./loop/pr-title-markers": "./src/loop/pr-title-markers.mjs",
|
|
45
47
|
"./loop/public-dev-loop-routing": "./src/loop/public-dev-loop-routing.mjs",
|
|
48
|
+
"./loop/public-dev-loop-routing-contract": "./src/loop/public-dev-loop-routing-contract.mjs",
|
|
46
49
|
"./loop/refinement-grill-state": "./src/loop/refinement-grill-state.mjs",
|
|
47
50
|
"./loop/queue-board-ordering": "./src/loop/queue-board-ordering.mjs",
|
|
51
|
+
"./loop/default-branch-guard": "./src/loop/default-branch-guard.mjs",
|
|
48
52
|
"./loop/queue-board-sync": "./src/loop/queue-board-sync.mjs",
|
|
49
53
|
"./loop/queue-driver": "./src/loop/queue-driver.mjs",
|
|
50
54
|
"./loop/queue-membership": "./src/loop/queue-membership.mjs",
|
|
@@ -68,7 +72,9 @@
|
|
|
68
72
|
"./projects/move-queue-item": "./src/projects/move-queue-item.mjs",
|
|
69
73
|
"./projects/resolve-project": "./src/projects/resolve-project.mjs",
|
|
70
74
|
"./harness": "./src/harness/index.mjs",
|
|
75
|
+
"./tracker": "./src/tracker/index.mjs",
|
|
71
76
|
"./loop/worktree-guard": "./src/loop/worktree-guard.mjs",
|
|
77
|
+
"./loop/main-checkout-ff": "./src/loop/main-checkout-ff.mjs",
|
|
72
78
|
"./loop/tracker-first-loop-state": "./src/loop/tracker-first-loop-state.mjs"
|
|
73
79
|
},
|
|
74
80
|
"bin": {
|
|
@@ -13,13 +13,21 @@
|
|
|
13
13
|
// T0: File-level analysis
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
|
+
// Extensionless dotfile configs: static allowlist, no content sniffing. Matched
|
|
17
|
+
// against the basename only, never a prefix/suffix guess. Deliberately just
|
|
18
|
+
// .devloops (the reported consumer shape): runtime-version files like .nvmrc
|
|
19
|
+
// were considered and REJECTED — classifying them config would let ci-guard/
|
|
20
|
+
// determinism carry stale clean verdicts across a runtime bump; unknown
|
|
21
|
+
// fails closed toward a full re-review, which is what a version bump needs.
|
|
22
|
+
const DOTFILE_CONFIG_BASENAMES = new Set([".devloops"]);
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* @typedef {object} T0Result
|
|
18
26
|
* @property {string[]} files — flat file paths
|
|
19
27
|
* @property {string[]} extensions — unique file extensions (lowercase, with dot)
|
|
20
28
|
* @property {string[]} directories — unique top-level path segments
|
|
21
29
|
* @property {boolean} renameOnly — true when all entries are renames (no adds/deletes/modifies)
|
|
22
|
-
* @property {boolean} allDocs — true when all files are under docs/ or
|
|
30
|
+
* @property {boolean} allDocs — true when all files are under docs/ or have a docs extension (.md/.markdown)
|
|
23
31
|
*/
|
|
24
32
|
|
|
25
33
|
/**
|
|
@@ -106,6 +114,9 @@ export function classifyFile(filePath) {
|
|
|
106
114
|
) {
|
|
107
115
|
return "config";
|
|
108
116
|
}
|
|
117
|
+
if (DOTFILE_CONFIG_BASENAMES.has(fp.split("/").pop())) {
|
|
118
|
+
return "config";
|
|
119
|
+
}
|
|
109
120
|
if (fp.includes(".test.") || fp.startsWith("test/")) {
|
|
110
121
|
return "test";
|
|
111
122
|
}
|
|
@@ -115,7 +126,10 @@ export function classifyFile(filePath) {
|
|
|
115
126
|
) {
|
|
116
127
|
return "code";
|
|
117
128
|
}
|
|
118
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
fp.startsWith("docs/") || fp.endsWith(".md") || fp.endsWith(".markdown") ||
|
|
131
|
+
fp === "README.md"
|
|
132
|
+
) {
|
|
119
133
|
return "docs";
|
|
120
134
|
}
|
|
121
135
|
return "unknown";
|
|
@@ -242,7 +256,9 @@ export function diffHasSecuritySeam(diffOutput) {
|
|
|
242
256
|
*
|
|
243
257
|
* Detects:
|
|
244
258
|
* - COMMENT_ONLY: only comment lines changed
|
|
245
|
-
* - DOCS_ONLY:
|
|
259
|
+
* - DOCS_ONLY: emitted when docs files are PRESENT in the diff (docs extensions
|
|
260
|
+
* .md/.markdown, or prose under docs/) — presence-based via
|
|
261
|
+
* t0PresentSurfaceCategories, not exclusivity
|
|
246
262
|
* - CONFIG_ONLY: only config files changed
|
|
247
263
|
* - TEST_ONLY: only test files changed
|
|
248
264
|
* - RENAME_ONLY: all renames, no content changes
|
|
@@ -265,6 +281,16 @@ export function analyzeT1(diffOutput, t0) {
|
|
|
265
281
|
let allChangedLinesAreNonLogic = true;
|
|
266
282
|
|
|
267
283
|
for (const line of lines) {
|
|
284
|
+
// A new file's header block (diff --git, index, --- a/..., +++ b/...) ends
|
|
285
|
+
// the previous file's hunk run. Resetting here is what lets the counting
|
|
286
|
+
// below treat EVERY +/- line inside a hunk as content: a removed line whose
|
|
287
|
+
// content itself starts with "--" (a CLI flag, a YAML document separator)
|
|
288
|
+
// renders as "---…" in the diff, and a prefix-based header exclusion would
|
|
289
|
+
// silently drop it from the counts.
|
|
290
|
+
if (line.startsWith("diff --git ")) {
|
|
291
|
+
inHunk = false;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
268
294
|
// Track hunk headers
|
|
269
295
|
if (line.startsWith("@@")) {
|
|
270
296
|
hunkCount++;
|
|
@@ -275,7 +301,7 @@ export function analyzeT1(diffOutput, t0) {
|
|
|
275
301
|
if (!inHunk) continue;
|
|
276
302
|
|
|
277
303
|
// Track line stats and classify
|
|
278
|
-
if (line.startsWith("+")
|
|
304
|
+
if (line.startsWith("+")) {
|
|
279
305
|
added++;
|
|
280
306
|
hasAnyChangedLine = true;
|
|
281
307
|
const content = line.slice(1).trim();
|
|
@@ -283,7 +309,7 @@ export function analyzeT1(diffOutput, t0) {
|
|
|
283
309
|
hasLogicChange = true;
|
|
284
310
|
allChangedLinesAreNonLogic = false;
|
|
285
311
|
}
|
|
286
|
-
} else if (line.startsWith("-")
|
|
312
|
+
} else if (line.startsWith("-")) {
|
|
287
313
|
deleted++;
|
|
288
314
|
hasAnyChangedLine = true;
|
|
289
315
|
const content = line.slice(1).trim();
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
extractRepoFlagsFromGhPrCreateSegments,
|
|
22
22
|
commandContainsRawExternalWrite,
|
|
23
23
|
extractRepoFlagsFromExternalWriteSegments,
|
|
24
|
+
commandContainsGitStash,
|
|
24
25
|
TARGET_REPO_SLUG,
|
|
25
26
|
} from "../loop/bash-command-classify.mjs";
|
|
26
27
|
|
|
@@ -84,6 +85,19 @@ export function decideBashGate({ command, repoSlug = null, gatePassed = false, g
|
|
|
84
85
|
if (typeof command !== "string") {
|
|
85
86
|
return ALLOW;
|
|
86
87
|
}
|
|
88
|
+
// `git stash` writes to `refs/stash`, one ref shared by every worktree over this repo's single
|
|
89
|
+
// `.git` directory — a stash from one worktree can pop into another's. Block it outright on the
|
|
90
|
+
// target repo; see skills/docs/worktree-guidance.md#never-git-stash-in-a-shared-git-layout for the
|
|
91
|
+
// stash-free alternative (git diff / a patch file / a scratch checkout).
|
|
92
|
+
if (commandContainsGitStash(command) && (repoSlug ?? "").toLowerCase() === TARGET_REPO_SLUG.toLowerCase()) {
|
|
93
|
+
return {
|
|
94
|
+
decision: "deny",
|
|
95
|
+
reason:
|
|
96
|
+
"git stash blocked: refs/stash is shared across every worktree over this repo's one .git directory, " +
|
|
97
|
+
"so a stash can pop into a different worktree. Use `git diff` (or `git diff --staged`), save changes " +
|
|
98
|
+
"to a patch file, or use a separate scratch worktree instead of stashing.",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
87
101
|
// Subagent-scoped external-write guard: block ad-hoc `gh issue create`/`gh issue comment`/
|
|
88
102
|
// `gh issue edit`/`gh pr comment` on the target repo from a subagent, so external writes flow through the
|
|
89
103
|
// sanctioned node wrappers. The main-agent/operator path (agentType null) is unaffected (#1051).
|
package/src/cli/primitives.mjs
CHANGED
|
@@ -126,13 +126,21 @@ export function parseIssueNumber(value, parseError = null) {
|
|
|
126
126
|
return parsePositiveInteger(value, "--issue", parseError);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
// `stdinText` is optional and additive: omit it and stdin stays closed exactly
|
|
130
|
+
// as before. Supply it (a `gh api ... --input -` payload) and it is piped in,
|
|
131
|
+
// so a caller that needs stdin no longer has to reach for a second, separately
|
|
132
|
+
// injected runner — every gh call in a script can route through the ONE
|
|
133
|
+
// runChild its tests already stub.
|
|
134
|
+
export function runChild(command, args, env = process.env, stdinText = undefined) {
|
|
130
135
|
return new Promise((resolve, reject) => {
|
|
131
|
-
const child = spawn(command, args, { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
136
|
+
const child = spawn(command, args, { env, stdio: [stdinText === undefined ? "ignore" : "pipe", "pipe", "pipe"] });
|
|
132
137
|
let stdout = "";
|
|
133
138
|
let stderr = "";
|
|
134
139
|
child.stdout.on("data", (chunk) => { stdout += String(chunk); });
|
|
135
140
|
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
|
141
|
+
if (stdinText !== undefined) {
|
|
142
|
+
child.stdin.end(stdinText);
|
|
143
|
+
}
|
|
136
144
|
child.on("error", reject);
|
|
137
145
|
child.on("close", (code) => { resolve({ code, stdout, stderr }); });
|
|
138
146
|
});
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* Only retries on usage/flag errors — NOT on network/auth/data errors.
|
|
9
9
|
*
|
|
10
10
|
* Detection contract:
|
|
11
|
-
* A usage/flag error is stderr JSON with
|
|
12
|
-
*
|
|
11
|
+
* A usage/flag error is stderr JSON with either the legacy
|
|
12
|
+
* { ok: false, usage: "..." } shape or the current short-error envelope
|
|
13
|
+
* { ok: false, hint: "..." } (formatCliError's `usage` presence check
|
|
14
|
+
* renders as `hint`, never the full usage text — see
|
|
15
|
+
* packages/core/src/github/review-threads.mjs's formatCliError), OR stderr
|
|
16
|
+
* text matching known argument-error patterns.
|
|
13
17
|
*
|
|
14
|
-
* Non-retryable: runtime errors ({ ok: false }
|
|
18
|
+
* Non-retryable: runtime errors ({ ok: false } with neither usage nor hint),
|
|
15
19
|
* network errors, auth errors, data errors, signal exits.
|
|
16
20
|
*
|
|
17
21
|
* Issue: #483
|
|
@@ -36,11 +40,15 @@ export function isUsageError(stderr) {
|
|
|
36
40
|
|
|
37
41
|
const trimmed = stderr.trim();
|
|
38
42
|
|
|
39
|
-
// Try JSON parse first: { ok: false, usage: "..." }
|
|
43
|
+
// Try JSON parse first: legacy { ok: false, usage: "..." }, or the current
|
|
44
|
+
// short-error envelope { ok: false, hint: "..." } (formatCliError never
|
|
45
|
+
// emits `usage` text anymore — a non-empty `hint` is its replacement
|
|
46
|
+
// presence signal that a usage string exists, fetchable via --help).
|
|
40
47
|
try {
|
|
41
48
|
const parsed = JSON.parse(trimmed);
|
|
42
|
-
if (parsed && parsed.ok === false
|
|
43
|
-
return true;
|
|
49
|
+
if (parsed && parsed.ok === false) {
|
|
50
|
+
if (typeof parsed.usage === 'string' && parsed.usage.length > 0) return true;
|
|
51
|
+
if (typeof parsed.hint === 'string' && parsed.hint.length > 0) return true;
|
|
44
52
|
}
|
|
45
53
|
} catch {
|
|
46
54
|
// Not JSON — fall through to pattern matching
|