dev-loops 0.2.5 → 0.2.7
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/.claude/.claude-plugin/plugin.json +9 -2
- package/.claude/agents/dev-loop.md +2 -1
- package/.claude/hooks/_bash-command-classify.mjs +164 -0
- package/.claude/hooks/_hook-decisions.mjs +130 -0
- package/.claude/hooks/_hook-io.mjs +1 -1
- package/.claude/hooks/_run-context.mjs +179 -0
- package/.claude/hooks/post-tool-use-merge.mjs +1 -1
- package/.claude/hooks/pre-tool-use-bash-gate.mjs +2 -2
- package/.claude/hooks/pre-tool-use-write-guard.mjs +1 -1
- package/.claude/skills/dev-loop/SKILL.md +5 -5
- package/.claude/skills/docs/merge-preconditions.md +15 -0
- package/.claude/skills/docs/pr-lifecycle-contract.md +1 -0
- package/CHANGELOG.md +52 -0
- package/agents/dev-loop.agent.md +5 -1
- package/cli/index.mjs +3 -1
- package/package.json +2 -2
- package/scripts/_cli-primitives.mjs +2 -0
- package/scripts/claude/generate-claude-assets.mjs +71 -3
- package/scripts/docs/validate-links.mjs +20 -11
- package/scripts/github/capture-review-threads.mjs +32 -14
- package/scripts/github/detect-checkpoint-evidence.mjs +28 -11
- package/scripts/github/detect-linked-issue-pr.mjs +22 -10
- package/scripts/github/manage-sub-issues.mjs +37 -16
- package/scripts/github/probe-copilot-review.mjs +24 -12
- package/scripts/github/ready-for-review.mjs +22 -10
- package/scripts/github/reconcile-draft-gate.mjs +22 -10
- package/scripts/github/reply-resolve-review-threads.mjs +34 -15
- package/scripts/github/request-copilot-review.mjs +28 -11
- package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
- package/scripts/github/stage-reviewer-draft.mjs +32 -14
- package/scripts/github/upsert-checkpoint-verdict.mjs +49 -26
- package/scripts/github/write-gate-findings-log.mjs +41 -20
- package/scripts/loop/_loop-pr-aggregation.mjs +45 -0
- package/scripts/loop/build-handoff-envelope.mjs +32 -14
- package/scripts/loop/conductor-monitor.mjs +26 -47
- package/scripts/loop/conductor.mjs +31 -12
- package/scripts/loop/copilot-pr-handoff.mjs +31 -14
- package/scripts/loop/debt-remediate.mjs +28 -11
- package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
- package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
- package/scripts/loop/detect-internal-only-pr.mjs +31 -13
- package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +28 -11
- package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
- package/scripts/loop/detect-stale-runner.mjs +32 -14
- package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
- package/scripts/loop/info.mjs +28 -10
- package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
- package/scripts/loop/inspect-run.mjs +35 -16
- package/scripts/loop/outer-loop.mjs +35 -16
- package/scripts/loop/pr-runner-coordination.mjs +31 -12
- package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
- package/scripts/loop/pre-flight-gate.mjs +25 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
- package/scripts/loop/pre-push-main-guard.mjs +19 -5
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
- package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
- package/scripts/loop/run-conductor-cycle.mjs +25 -47
- package/scripts/loop/run-refinement-audit.mjs +44 -22
- package/scripts/loop/run-watch-cycle.mjs +28 -12
- package/scripts/loop/steer-loop.mjs +122 -62
- package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
- package/scripts/projects/archive-done-items.mjs +410 -0
- package/scripts/projects/reorder-queue-item.mjs +334 -76
- package/scripts/refine/_refine-helpers.mjs +21 -9
- package/scripts/refine/verify.mjs +31 -13
- package/skills/dev-loop/SKILL.md +9 -5
- package/skills/docs/merge-preconditions.md +15 -0
- package/skills/docs/pr-lifecycle-contract.md +1 -0
package/agents/dev-loop.agent.md
CHANGED
|
@@ -25,7 +25,11 @@ The envelope is the primary handoff artifact — it is derived from resolver out
|
|
|
25
25
|
- `acceptance` — self-validation criteria for declaring completion
|
|
26
26
|
|
|
27
27
|
**Construction sequence:**
|
|
28
|
-
|
|
28
|
+
<!-- pi-only -->
|
|
29
|
+
**CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>` from this agent's own installed path: this agent is installed at `<package-root>/.pi/agents/dev-loop.agent.md`, so the package root is `../..` from this agent's directory (`agents` → `.pi` → package root). (The `dev-loop` skill resolves it analogously from its own installed path.)
|
|
30
|
+
<!-- /pi-only -->
|
|
31
|
+
|
|
32
|
+
1. Run the deterministic startup resolver to produce the authoritative state bundle: `node <dev-loops-package-root>/cli/index.mjs loop startup --issue <n>` for issues, or `node <dev-loops-package-root>/cli/index.mjs loop startup --pr <n>` for PRs.
|
|
29
33
|
2. Pass the resolver output, resolved settings (merged from `.devloops` and `.pi/dev-loop/defaults.yaml`), and current gate state to `buildDevLoopHandoffEnvelope()`.
|
|
30
34
|
3. **Validate the envelope** with `validateHandoffEnvelope()` before consuming any field. If validation returns `ok: false`, reject the handoff with the structured error — do not load requiredReads, do not execute nextAction, do not delegate.
|
|
31
35
|
4. Read the envelope as the first artifact.
|
package/cli/index.mjs
CHANGED
|
@@ -52,6 +52,7 @@ const SUBCOMMAND_ROUTES = {
|
|
|
52
52
|
add: "scripts/projects/add-queue-item.mjs",
|
|
53
53
|
move: "scripts/projects/move-queue-item.mjs",
|
|
54
54
|
reorder: "scripts/projects/reorder-queue-item.mjs",
|
|
55
|
+
"archive-done": "scripts/projects/archive-done-items.mjs",
|
|
55
56
|
ensure: "scripts/projects/ensure-queue-board.mjs",
|
|
56
57
|
},
|
|
57
58
|
queue: {
|
|
@@ -113,7 +114,8 @@ const SUBCOMMAND_DESCRIPTIONS = {
|
|
|
113
114
|
list: "List queue board items",
|
|
114
115
|
add: "Add issue/PR to queue board",
|
|
115
116
|
move: "Move queue item between Status columns",
|
|
116
|
-
reorder: "Reorder
|
|
117
|
+
reorder: "Reorder items (move-to-top/move-after/order, --dry-run)",
|
|
118
|
+
"archive-done": "Archive closed Done items older than a duration",
|
|
117
119
|
ensure: "Create/repair queue board bootstrap surface",
|
|
118
120
|
},
|
|
119
121
|
queue: {
|
package/package.json
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
70
|
"mermaid": "11.15.0",
|
|
71
|
-
"@dev-loops/core": "^0.2.
|
|
71
|
+
"@dev-loops/core": "^0.2.7"
|
|
72
72
|
},
|
|
73
73
|
"repository": {
|
|
74
74
|
"type": "git",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"url": "https://github.com/mfittko/dev-loops/issues"
|
|
79
79
|
},
|
|
80
80
|
"homepage": "https://github.com/mfittko/dev-loops#readme",
|
|
81
|
-
"version": "0.2.
|
|
81
|
+
"version": "0.2.7",
|
|
82
82
|
"files": [
|
|
83
83
|
"cli/",
|
|
84
84
|
"lib/",
|
|
@@ -24,6 +24,16 @@ import { transformAgent, transformSkill, stripPiOnlyBlocks } from "@dev-loops/co
|
|
|
24
24
|
export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
|
|
25
25
|
const assets = [];
|
|
26
26
|
|
|
27
|
+
// The dev-loops package version pins the Claude `npx dev-loops@<version>` CLI invocation so the
|
|
28
|
+
// generated plugin tree cannot drift against the published version (#833). Read it once here.
|
|
29
|
+
// Falls back to `latest` when no repo-root package.json is present (e.g. a consumer/fixture tree
|
|
30
|
+
// that mirrors only agents/skills).
|
|
31
|
+
let version = "latest";
|
|
32
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
33
|
+
if (fs.existsSync(pkgPath)) {
|
|
34
|
+
version = JSON.parse(fs.readFileSync(pkgPath, "utf8")).version ?? "latest";
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
const agentsDir = path.join(repoRoot, "agents");
|
|
28
38
|
if (fs.existsSync(agentsDir)) {
|
|
29
39
|
for (const entry of fs.readdirSync(agentsDir).sort()) {
|
|
@@ -31,7 +41,7 @@ export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
|
|
|
31
41
|
const source = `agents/${entry}`;
|
|
32
42
|
const raw = fs.readFileSync(path.join(repoRoot, source), "utf8");
|
|
33
43
|
const base = entry.slice(0, -".agent.md".length);
|
|
34
|
-
assets.push({ target: `.claude/agents/${base}.md`, content: transformAgent({ source, raw }) });
|
|
44
|
+
assets.push({ target: `.claude/agents/${base}.md`, content: transformAgent({ source, raw, version }) });
|
|
35
45
|
}
|
|
36
46
|
}
|
|
37
47
|
|
|
@@ -43,7 +53,7 @@ export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
|
|
|
43
53
|
const abs = path.join(repoRoot, source);
|
|
44
54
|
if (!fs.existsSync(abs)) continue; // e.g. skills/docs/ holds shared docs, not a SKILL.md
|
|
45
55
|
const raw = fs.readFileSync(abs, "utf8");
|
|
46
|
-
assets.push({ target: `.claude/skills/${entry.name}/SKILL.md`, content: transformSkill({ source, raw }) });
|
|
56
|
+
assets.push({ target: `.claude/skills/${entry.name}/SKILL.md`, content: transformSkill({ source, raw, version }) });
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -60,9 +70,54 @@ export function collectGeneratedAssets({ repoRoot = process.cwd() } = {}) {
|
|
|
60
70
|
assets.push(...collectBundle(repoRoot, srcRel, targetRel));
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
// Self-contained hook bundle (#843). The PreToolUse/PostToolUse hook scripts under
|
|
74
|
+
// .claude/hooks/ ship inside the Claude plugin, which has no node_modules — so they cannot
|
|
75
|
+
// import `@dev-loops/core` (it is unresolvable from the plugin cache and crashes the hook on
|
|
76
|
+
// load). Vendor the pure deciders/classifiers they need as self-contained relative `_*.mjs`
|
|
77
|
+
// modules generated from the canonical core sources, with cross-module imports rewritten to
|
|
78
|
+
// local paths. The no-drift check keeps them in sync with packages/core/src.
|
|
79
|
+
assets.push(...collectHookBundle(repoRoot));
|
|
80
|
+
|
|
63
81
|
return assets;
|
|
64
82
|
}
|
|
65
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The core modules vendored into `.claude/hooks/` for the self-contained hook bundle (#843).
|
|
86
|
+
* `rewrites` rewrites cross-module import specifiers to the sibling vendored copies; node:
|
|
87
|
+
* builtins are left untouched.
|
|
88
|
+
*/
|
|
89
|
+
const HOOK_BUNDLE = [
|
|
90
|
+
{ source: "packages/core/src/loop/bash-command-classify.mjs", target: ".claude/hooks/_bash-command-classify.mjs", rewrites: [] },
|
|
91
|
+
{ source: "packages/core/src/loop/run-context.mjs", target: ".claude/hooks/_run-context.mjs", rewrites: [] },
|
|
92
|
+
{
|
|
93
|
+
source: "packages/core/src/claude/hook-decisions.mjs",
|
|
94
|
+
target: ".claude/hooks/_hook-decisions.mjs",
|
|
95
|
+
rewrites: [
|
|
96
|
+
['"../loop/run-context.mjs"', '"./_run-context.mjs"'],
|
|
97
|
+
['"../loop/bash-command-classify.mjs"', '"./_bash-command-classify.mjs"'],
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/** Marker that identifies a generated hook-bundle module (a JS comment, distinct from the
|
|
103
|
+
* markdown `<!-- GENERATED -->` banner used by generated skills/agents/docs). Used both to
|
|
104
|
+
* stamp generated bundle modules and to scope orphan detection to them within `.claude/hooks/`. */
|
|
105
|
+
const HOOK_BUNDLE_BANNER_PREFIX = "// GENERATED from ";
|
|
106
|
+
|
|
107
|
+
/** Collect the vendored self-contained hook bundle modules (#843). */
|
|
108
|
+
function collectHookBundle(repoRoot) {
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const { source, target, rewrites } of HOOK_BUNDLE) {
|
|
111
|
+
const abs = path.join(repoRoot, source);
|
|
112
|
+
if (!fs.existsSync(abs)) continue; // no-op when core sources are absent (e.g. consumer tree)
|
|
113
|
+
let body = fs.readFileSync(abs, "utf8");
|
|
114
|
+
for (const [from, to] of rewrites) body = body.split(from).join(to);
|
|
115
|
+
const banner = `${HOOK_BUNDLE_BANNER_PREFIX}${source} by scripts/claude/generate-claude-assets.mjs — do not edit; edit the source and regenerate.\n`;
|
|
116
|
+
out.push({ target, content: banner + body });
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
66
121
|
/**
|
|
67
122
|
* Recursively collect `*.md` files under a source dir as {target, content} bundle assets.
|
|
68
123
|
* Bodies are passed through `stripPiOnlyBlocks` so bundled contract docs can scope Pi-runtime
|
|
@@ -114,10 +169,23 @@ function listFilesRecursive(repoRoot, rel) {
|
|
|
114
169
|
* bundled docs/templates whose source was removed — are detected, not just SKILL.md files.
|
|
115
170
|
*/
|
|
116
171
|
function listExistingAssetFiles(repoRoot) {
|
|
117
|
-
|
|
172
|
+
const files = [
|
|
118
173
|
...listFilesRecursive(repoRoot, ".claude/agents"),
|
|
119
174
|
...listFilesRecursive(repoRoot, ".claude/skills"),
|
|
120
175
|
];
|
|
176
|
+
// `.claude/hooks/` mixes hand-authored scripts (hooks.json, _hook-io.mjs, the three hook
|
|
177
|
+
// scripts) with generated bundle modules (#843). Only the generated ones — identified by the
|
|
178
|
+
// generator banner — participate in orphan detection, so a dropped/renamed HOOK_BUNDLE entry
|
|
179
|
+
// is caught without false-flagging the hand-authored files.
|
|
180
|
+
for (const rel of listFilesRecursive(repoRoot, ".claude/hooks")) {
|
|
181
|
+
const abs = path.join(repoRoot, rel);
|
|
182
|
+
try {
|
|
183
|
+
if (fs.readFileSync(abs, "utf8").startsWith(HOOK_BUNDLE_BANNER_PREFIX)) files.push(rel);
|
|
184
|
+
} catch {
|
|
185
|
+
// unreadable — skip
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return files;
|
|
121
189
|
}
|
|
122
190
|
|
|
123
191
|
/**
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import { lstat, readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
5
6
|
|
|
6
7
|
import { isDirectCliRun } from "../_core-helpers.mjs";
|
|
7
|
-
import {
|
|
8
|
+
import { requireTokenValue } from "../_cli-primitives.mjs";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_SCAN_PATHS = Object.freeze([
|
|
10
11
|
"README.md",
|
|
@@ -52,26 +53,34 @@ function parseError(message) {
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export function parseValidateLinksCliArgs(argv) {
|
|
55
|
-
const
|
|
56
|
+
const { tokens } = parseArgs({
|
|
57
|
+
args: [...argv],
|
|
58
|
+
options: { help: { type: "boolean", short: "h" }, root: { type: "string" } },
|
|
59
|
+
allowPositionals: true,
|
|
60
|
+
strict: false,
|
|
61
|
+
tokens: true,
|
|
62
|
+
});
|
|
56
63
|
const options = {
|
|
57
64
|
help: false,
|
|
58
65
|
repoRoot: resolveDefaultRepoRoot(),
|
|
59
66
|
};
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
for (const token of tokens) {
|
|
69
|
+
if (token.kind === "positional") {
|
|
70
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
71
|
+
}
|
|
72
|
+
if (token.kind !== "option") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (token.name === "help") {
|
|
65
76
|
options.help = true;
|
|
66
77
|
return options;
|
|
67
78
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
options.repoRoot = path.resolve(requireOptionValue(args, "--root"));
|
|
79
|
+
if (token.name === "root") {
|
|
80
|
+
options.repoRoot = path.resolve(requireTokenValue(token));
|
|
71
81
|
continue;
|
|
72
82
|
}
|
|
73
|
-
|
|
74
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
83
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
return options;
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
parseReviewThreads,
|
|
9
9
|
readInput,
|
|
10
10
|
} from "../_core-helpers.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { parseArgs } from "node:util";
|
|
12
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
12
13
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
13
14
|
export const REVIEW_THREADS_QUERY = [
|
|
14
15
|
"query($owner: String!, $name: String!, $pr: Int!) {",
|
|
@@ -49,7 +50,19 @@ Exit codes:
|
|
|
49
50
|
1 Error
|
|
50
51
|
`;
|
|
51
52
|
export function parseCaptureCliArgs(argv) {
|
|
52
|
-
const
|
|
53
|
+
const { tokens } = parseArgs({
|
|
54
|
+
args: [...argv],
|
|
55
|
+
options: {
|
|
56
|
+
help: { type: "boolean", short: "h" },
|
|
57
|
+
input: { type: "string" },
|
|
58
|
+
output: { type: "string" },
|
|
59
|
+
repo: { type: "string" },
|
|
60
|
+
pr: { type: "string" },
|
|
61
|
+
},
|
|
62
|
+
allowPositionals: true,
|
|
63
|
+
strict: false,
|
|
64
|
+
tokens: true,
|
|
65
|
+
});
|
|
53
66
|
const options = {
|
|
54
67
|
inputPath: undefined,
|
|
55
68
|
outputPath: undefined,
|
|
@@ -57,29 +70,34 @@ export function parseCaptureCliArgs(argv) {
|
|
|
57
70
|
pr: undefined,
|
|
58
71
|
help: false,
|
|
59
72
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
for (const token of tokens) {
|
|
74
|
+
if (token.kind === "positional") {
|
|
75
|
+
throw new Error(`Unknown argument: ${token.value}`);
|
|
76
|
+
}
|
|
77
|
+
if (token.kind !== "option") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (token.name === "help") {
|
|
63
81
|
options.help = true;
|
|
64
82
|
return options;
|
|
65
83
|
}
|
|
66
|
-
if (token === "
|
|
67
|
-
options.inputPath =
|
|
84
|
+
if (token.name === "input") {
|
|
85
|
+
options.inputPath = requireTokenValue(token);
|
|
68
86
|
continue;
|
|
69
87
|
}
|
|
70
|
-
if (token === "
|
|
71
|
-
options.outputPath =
|
|
88
|
+
if (token.name === "output") {
|
|
89
|
+
options.outputPath = requireTokenValue(token);
|
|
72
90
|
continue;
|
|
73
91
|
}
|
|
74
|
-
if (token === "
|
|
75
|
-
options.repo =
|
|
92
|
+
if (token.name === "repo") {
|
|
93
|
+
options.repo = requireTokenValue(token);
|
|
76
94
|
continue;
|
|
77
95
|
}
|
|
78
|
-
if (token === "
|
|
79
|
-
options.pr = parsePrNumber(
|
|
96
|
+
if (token.name === "pr") {
|
|
97
|
+
options.pr = parsePrNumber(requireTokenValue(token));
|
|
80
98
|
continue;
|
|
81
99
|
}
|
|
82
|
-
throw new Error(`Unknown argument: ${token}`);
|
|
100
|
+
throw new Error(`Unknown argument: ${token.rawName}`);
|
|
83
101
|
}
|
|
84
102
|
const hasLiveArgs = options.repo !== undefined || options.pr !== undefined;
|
|
85
103
|
const hasCompleteLiveArgs = options.repo !== undefined && options.pr !== undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import {
|
|
3
4
|
buildParseError,
|
|
4
5
|
formatCliError,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
summarizeGateReviewCommentMarkers,
|
|
9
10
|
summarizeGateReviewComments,
|
|
10
11
|
} from "../_core-helpers.mjs";
|
|
11
|
-
import { parsePrNumber,
|
|
12
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
12
13
|
import { fetchGithubReviewThreadsPayload } from "./capture-review-threads.mjs";
|
|
13
14
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
14
15
|
import { ensureAsyncRunnerOwnership } from "../loop/_pr-runner-coordination.mjs";
|
|
@@ -84,30 +85,46 @@ Exit codes:
|
|
|
84
85
|
1 Argument error, gh failure, malformed gh JSON, or missing required pre-merge gate evidence.`.trim();
|
|
85
86
|
const parseError = buildParseError(USAGE);
|
|
86
87
|
export function parseDetectCheckpointEvidenceCliArgs(argv) {
|
|
87
|
-
const
|
|
88
|
+
const { tokens } = parseArgs({
|
|
89
|
+
args: [...argv],
|
|
90
|
+
options: {
|
|
91
|
+
help: { type: "boolean", short: "h" },
|
|
92
|
+
repo: { type: "string" },
|
|
93
|
+
pr: { type: "string" },
|
|
94
|
+
"require-before-merge": { type: "boolean" },
|
|
95
|
+
},
|
|
96
|
+
allowPositionals: true,
|
|
97
|
+
strict: false,
|
|
98
|
+
tokens: true,
|
|
99
|
+
});
|
|
88
100
|
const options = {
|
|
89
101
|
help: false,
|
|
90
102
|
repo: undefined,
|
|
91
103
|
pr: undefined,
|
|
92
104
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
for (const token of tokens) {
|
|
106
|
+
if (token.kind === "positional") {
|
|
107
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
108
|
+
}
|
|
109
|
+
if (token.kind !== "option") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (token.name === "help") {
|
|
96
113
|
options.help = true;
|
|
97
114
|
return options;
|
|
98
115
|
}
|
|
99
|
-
if (token === "
|
|
100
|
-
options.repo =
|
|
116
|
+
if (token.name === "repo") {
|
|
117
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
101
118
|
continue;
|
|
102
119
|
}
|
|
103
|
-
if (token === "
|
|
104
|
-
options.pr = parsePrNumber(
|
|
120
|
+
if (token.name === "pr") {
|
|
121
|
+
options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
|
|
105
122
|
continue;
|
|
106
123
|
}
|
|
107
|
-
if (token === "
|
|
124
|
+
if (token.name === "require-before-merge") {
|
|
108
125
|
throw parseError(`--require-before-merge has been removed: gate evidence enforcement is now always-on by default. Omit the flag.`);
|
|
109
126
|
}
|
|
110
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
127
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
111
128
|
}
|
|
112
129
|
if (options.repo === undefined || options.pr === undefined) {
|
|
113
130
|
throw parseError("detect-checkpoint-evidence requires both --repo <owner/name> and --pr <number>");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parseIssueNumber,
|
|
4
|
+
import { parseIssueNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
export const LINKED_ISSUE_PR_QUERY = [
|
|
6
7
|
"query($owner:String!, $name:String!, $issue:Int!, $after:String) {",
|
|
@@ -75,27 +76,38 @@ Error output (stderr, JSON):
|
|
|
75
76
|
{ "ok": false, "error": "..." }`.trim();
|
|
76
77
|
const parseError = buildParseError(USAGE);
|
|
77
78
|
export function parseDetectLinkedIssuePrCliArgs(argv) {
|
|
78
|
-
const
|
|
79
|
+
const { tokens } = parseArgs({
|
|
80
|
+
args: [...argv],
|
|
81
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, issue: { type: "string" } },
|
|
82
|
+
allowPositionals: true,
|
|
83
|
+
strict: false,
|
|
84
|
+
tokens: true,
|
|
85
|
+
});
|
|
79
86
|
const options = {
|
|
80
87
|
help: false,
|
|
81
88
|
repo: undefined,
|
|
82
89
|
issue: undefined,
|
|
83
90
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
for (const token of tokens) {
|
|
92
|
+
if (token.kind === "positional") {
|
|
93
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
94
|
+
}
|
|
95
|
+
if (token.kind !== "option") {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (token.name === "help") {
|
|
87
99
|
options.help = true;
|
|
88
100
|
return options;
|
|
89
101
|
}
|
|
90
|
-
if (token === "
|
|
91
|
-
options.repo =
|
|
102
|
+
if (token.name === "repo") {
|
|
103
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
92
104
|
continue;
|
|
93
105
|
}
|
|
94
|
-
if (token === "
|
|
95
|
-
options.issue = parseIssueNumber(
|
|
106
|
+
if (token.name === "issue") {
|
|
107
|
+
options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
|
|
96
108
|
continue;
|
|
97
109
|
}
|
|
98
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
110
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
99
111
|
}
|
|
100
112
|
if (options.repo === undefined || options.issue === undefined) {
|
|
101
113
|
throw parseError("Linked PR detection requires both --repo <owner/name> and --issue <number>");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
3
|
-
import { parsePositiveInteger,
|
|
4
|
+
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
const USAGE = `Usage: manage-sub-issues.mjs <command> --repo <owner/name> --issue <number> [options]
|
|
6
7
|
Deterministic helper for reading, linking, ordering, and verifying GitHub sub-issue trees.
|
|
@@ -76,37 +77,57 @@ export function parseManageSubIssuesCliArgs(argv) {
|
|
|
76
77
|
expected: undefined,
|
|
77
78
|
ordered: false,
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const { tokens } = parseArgs({
|
|
81
|
+
args,
|
|
82
|
+
options: {
|
|
83
|
+
help: { type: "boolean", short: "h" },
|
|
84
|
+
repo: { type: "string" },
|
|
85
|
+
issue: { type: "string" },
|
|
86
|
+
child: { type: "string" },
|
|
87
|
+
order: { type: "string" },
|
|
88
|
+
expected: { type: "string" },
|
|
89
|
+
ordered: { type: "boolean" },
|
|
90
|
+
},
|
|
91
|
+
allowPositionals: true,
|
|
92
|
+
strict: false,
|
|
93
|
+
tokens: true,
|
|
94
|
+
});
|
|
95
|
+
for (const token of tokens) {
|
|
96
|
+
if (token.kind === "positional") {
|
|
97
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
98
|
+
}
|
|
99
|
+
if (token.kind !== "option") {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (token.name === "help") {
|
|
82
103
|
options.help = true;
|
|
83
104
|
return options;
|
|
84
105
|
}
|
|
85
|
-
if (token === "
|
|
86
|
-
options.repo =
|
|
106
|
+
if (token.name === "repo") {
|
|
107
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
87
108
|
continue;
|
|
88
109
|
}
|
|
89
|
-
if (token === "
|
|
90
|
-
options.issue = parsePositiveInteger(
|
|
110
|
+
if (token.name === "issue") {
|
|
111
|
+
options.issue = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
|
|
91
112
|
continue;
|
|
92
113
|
}
|
|
93
|
-
if (token === "
|
|
94
|
-
options.child = parsePositiveInteger(
|
|
114
|
+
if (token.name === "child") {
|
|
115
|
+
options.child = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
|
|
95
116
|
continue;
|
|
96
117
|
}
|
|
97
|
-
if (token === "
|
|
98
|
-
options.order = parseIssueList(
|
|
118
|
+
if (token.name === "order") {
|
|
119
|
+
options.order = parseIssueList(requireTokenValue(token, parseError));
|
|
99
120
|
continue;
|
|
100
121
|
}
|
|
101
|
-
if (token === "
|
|
102
|
-
options.expected = parseIssueList(
|
|
122
|
+
if (token.name === "expected") {
|
|
123
|
+
options.expected = parseIssueList(requireTokenValue(token, parseError));
|
|
103
124
|
continue;
|
|
104
125
|
}
|
|
105
|
-
if (token === "
|
|
126
|
+
if (token.name === "ordered") {
|
|
106
127
|
options.ordered = true;
|
|
107
128
|
continue;
|
|
108
129
|
}
|
|
109
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
130
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
110
131
|
}
|
|
111
132
|
if (options.repo === undefined || options.issue === undefined) {
|
|
112
133
|
throw parseError("Both --repo <owner/name> and --issue <number> are required");
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
3
3
|
import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, parseJsonText, parseReviewThreads } from "../_core-helpers.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
5
6
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
7
|
import {
|
|
7
8
|
DEFAULT_POLL_INTERVAL_MS,
|
|
@@ -88,7 +89,13 @@ function rejectRemovedFlag(token) {
|
|
|
88
89
|
);
|
|
89
90
|
}
|
|
90
91
|
export function parseWatchCliArgs(argv) {
|
|
91
|
-
const
|
|
92
|
+
const { tokens } = parseArgs({
|
|
93
|
+
args: [...argv],
|
|
94
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
|
|
95
|
+
allowPositionals: true,
|
|
96
|
+
strict: false,
|
|
97
|
+
tokens: true,
|
|
98
|
+
});
|
|
92
99
|
const options = {
|
|
93
100
|
help: false,
|
|
94
101
|
repo: undefined,
|
|
@@ -96,24 +103,29 @@ export function parseWatchCliArgs(argv) {
|
|
|
96
103
|
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
97
104
|
timeoutMs: COPILOT_REVIEW_WAIT_TIMEOUT_MS,
|
|
98
105
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
106
|
+
for (const token of tokens) {
|
|
107
|
+
if (token.kind === "positional") {
|
|
108
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
109
|
+
}
|
|
110
|
+
if (token.kind !== "option") {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (token.name === "help") {
|
|
102
114
|
options.help = true;
|
|
103
115
|
return options;
|
|
104
116
|
}
|
|
105
|
-
if (REMOVED_FLAGS.has(token)) {
|
|
106
|
-
rejectRemovedFlag(token);
|
|
117
|
+
if (REMOVED_FLAGS.has(token.rawName)) {
|
|
118
|
+
rejectRemovedFlag(token.rawName);
|
|
107
119
|
}
|
|
108
|
-
if (token === "
|
|
109
|
-
options.repo =
|
|
120
|
+
if (token.name === "repo") {
|
|
121
|
+
options.repo = requireTokenValue(token, parseError).trim();
|
|
110
122
|
continue;
|
|
111
123
|
}
|
|
112
|
-
if (token === "
|
|
113
|
-
options.pr = parsePositiveInteger(
|
|
124
|
+
if (token.name === "pr") {
|
|
125
|
+
options.pr = parsePositiveInteger(requireTokenValue(token, parseError), "--pr", parseError);
|
|
114
126
|
continue;
|
|
115
127
|
}
|
|
116
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
128
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
117
129
|
}
|
|
118
130
|
if (options.repo === undefined || options.pr === undefined) {
|
|
119
131
|
throw parseError("Watching Copilot review requires both --repo <owner/name> and --pr <number>");
|
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
2
3
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText, summarizeGateReviewComments, summarizeGateReviewCommentMarkers } from "../_core-helpers.mjs";
|
|
3
|
-
import { parsePrNumber,
|
|
4
|
+
import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
4
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
import { loadDevLoopConfig, resolveGateConfig } from "@dev-loops/core/config";
|
|
7
|
+
import { findBlockingTitleMarkers } from "@dev-loops/core/loop/pr-title-markers";
|
|
6
8
|
|
|
7
9
|
const USAGE = `Usage: ready-for-review.mjs --repo <owner/name> --pr <number>\nWrapper around gh pr ready that enforces gate-evidence validation.`;
|
|
8
10
|
const parseError = buildParseError(USAGE);
|
|
9
|
-
const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus } } }`;
|
|
11
|
+
const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state, mergeStateStatus, title } } }`;
|
|
10
12
|
|
|
11
13
|
export function parseReadyForReviewCliArgs(argv) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const { tokens } = parseArgs({
|
|
15
|
+
args: [...argv],
|
|
16
|
+
options: { help: { type: "boolean", short: "h" }, repo: { type: "string" }, pr: { type: "string" } },
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
strict: false,
|
|
19
|
+
tokens: true,
|
|
20
|
+
});
|
|
21
|
+
const opts = { help: false, repo: undefined, pr: undefined };
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
if (token.kind === "positional") throw parseError(`Unknown argument: ${token.value}`);
|
|
24
|
+
if (token.kind !== "option") continue;
|
|
25
|
+
if (token.name === "help") { opts.help = true; return opts; }
|
|
26
|
+
if (token.name === "repo") { opts.repo = requireTokenValue(token, parseError).trim(); continue; }
|
|
27
|
+
if (token.name === "pr") { opts.pr = parsePrNumber(requireTokenValue(token, parseError), parseError); continue; }
|
|
28
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
19
29
|
}
|
|
20
30
|
if (!opts.repo || opts.pr === undefined) throw parseError("ready-for-review requires --repo and --pr");
|
|
21
31
|
parseRepoSlug(opts.repo);
|
|
@@ -33,7 +43,7 @@ async function fetchPrState({ repo, pr }, { env, ghCommand }) {
|
|
|
33
43
|
const r = await runGhJson(["api", "graphql", "-f", `query=${PR_VIEW_QUERY}`, "-f", `owner=${owner}`, "-f", `name=${name}`, "-F", `number=${pr}`], { env, ghCommand });
|
|
34
44
|
const d = r?.data?.repository?.pullRequest;
|
|
35
45
|
if (!d) throw new Error(`Could not fetch PR #${pr}`);
|
|
36
|
-
return { id: d.id, isDraft: d.isDraft === true, headRefOid: typeof d.headRefOid === "string" ? d.headRefOid.trim() : null, state: typeof d.state === "string" ? d.state.trim() : null, mergeStateStatus: typeof d.mergeStateStatus === "string" ? d.mergeStateStatus.trim() : null };
|
|
46
|
+
return { id: d.id, isDraft: d.isDraft === true, headRefOid: typeof d.headRefOid === "string" ? d.headRefOid.trim() : null, state: typeof d.state === "string" ? d.state.trim() : null, mergeStateStatus: typeof d.mergeStateStatus === "string" ? d.mergeStateStatus.trim() : null, title: typeof d.title === "string" ? d.title : null };
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
async function fetchCiStatus({ repo, pr }, { env, ghCommand }) {
|
|
@@ -71,6 +81,8 @@ export async function readyForReview(options, { env = process.env, ghCommand = "
|
|
|
71
81
|
const headSha = prState.headRefOid;
|
|
72
82
|
if (!headSha) throw new Error(`Could not resolve head SHA`);
|
|
73
83
|
if (!prState.isDraft) throw new Error(`PR #${options.pr} is not in draft state`);
|
|
84
|
+
const titleMarkers = findBlockingTitleMarkers(prState.title);
|
|
85
|
+
if (titleMarkers.length > 0) throw new Error(`PR #${options.pr} cannot be marked ready: title contains merge-blocking marker(s): ${titleMarkers.join(", ")}. Remove them from the title first.`);
|
|
74
86
|
if (requireCi) { const ci = await fetchCiStatus({ repo: options.repo, pr: options.pr }, { env, ghCommand }); if (ci.status === "blocked") throw new Error(`PR #${options.pr} has blocking CI checks`); if (ci.status !== "success") throw new Error(`PR #${options.pr} CI is not green`); }
|
|
75
87
|
const gate = await fetchGateEvidence({ repo: options.repo, pr: options.pr, headSha }, { env, ghCommand });
|
|
76
88
|
if (!gate.cleanEvidenceExists && !gate.effectiveHeadClean) throw new Error(`No visible clean draft_gate evidence on ${headSha.slice(0,7)}`);
|