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.
Files changed (70) hide show
  1. package/.claude/.claude-plugin/plugin.json +9 -2
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/hooks/_bash-command-classify.mjs +164 -0
  4. package/.claude/hooks/_hook-decisions.mjs +130 -0
  5. package/.claude/hooks/_hook-io.mjs +1 -1
  6. package/.claude/hooks/_run-context.mjs +179 -0
  7. package/.claude/hooks/post-tool-use-merge.mjs +1 -1
  8. package/.claude/hooks/pre-tool-use-bash-gate.mjs +2 -2
  9. package/.claude/hooks/pre-tool-use-write-guard.mjs +1 -1
  10. package/.claude/skills/dev-loop/SKILL.md +5 -5
  11. package/.claude/skills/docs/merge-preconditions.md +15 -0
  12. package/.claude/skills/docs/pr-lifecycle-contract.md +1 -0
  13. package/CHANGELOG.md +52 -0
  14. package/agents/dev-loop.agent.md +5 -1
  15. package/cli/index.mjs +3 -1
  16. package/package.json +2 -2
  17. package/scripts/_cli-primitives.mjs +2 -0
  18. package/scripts/claude/generate-claude-assets.mjs +71 -3
  19. package/scripts/docs/validate-links.mjs +20 -11
  20. package/scripts/github/capture-review-threads.mjs +32 -14
  21. package/scripts/github/detect-checkpoint-evidence.mjs +28 -11
  22. package/scripts/github/detect-linked-issue-pr.mjs +22 -10
  23. package/scripts/github/manage-sub-issues.mjs +37 -16
  24. package/scripts/github/probe-copilot-review.mjs +24 -12
  25. package/scripts/github/ready-for-review.mjs +22 -10
  26. package/scripts/github/reconcile-draft-gate.mjs +22 -10
  27. package/scripts/github/reply-resolve-review-threads.mjs +34 -15
  28. package/scripts/github/request-copilot-review.mjs +28 -11
  29. package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
  30. package/scripts/github/stage-reviewer-draft.mjs +32 -14
  31. package/scripts/github/upsert-checkpoint-verdict.mjs +49 -26
  32. package/scripts/github/write-gate-findings-log.mjs +41 -20
  33. package/scripts/loop/_loop-pr-aggregation.mjs +45 -0
  34. package/scripts/loop/build-handoff-envelope.mjs +32 -14
  35. package/scripts/loop/conductor-monitor.mjs +26 -47
  36. package/scripts/loop/conductor.mjs +31 -12
  37. package/scripts/loop/copilot-pr-handoff.mjs +31 -14
  38. package/scripts/loop/debt-remediate.mjs +28 -11
  39. package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
  40. package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
  41. package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
  42. package/scripts/loop/detect-internal-only-pr.mjs +31 -13
  43. package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
  44. package/scripts/loop/detect-pr-gate-coordination-state.mjs +28 -11
  45. package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
  46. package/scripts/loop/detect-stale-runner.mjs +32 -14
  47. package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
  48. package/scripts/loop/info.mjs +28 -10
  49. package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
  50. package/scripts/loop/inspect-run.mjs +35 -16
  51. package/scripts/loop/outer-loop.mjs +35 -16
  52. package/scripts/loop/pr-runner-coordination.mjs +31 -12
  53. package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
  54. package/scripts/loop/pre-flight-gate.mjs +25 -9
  55. package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
  56. package/scripts/loop/pre-push-main-guard.mjs +19 -5
  57. package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
  58. package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
  59. package/scripts/loop/run-conductor-cycle.mjs +25 -47
  60. package/scripts/loop/run-refinement-audit.mjs +44 -22
  61. package/scripts/loop/run-watch-cycle.mjs +28 -12
  62. package/scripts/loop/steer-loop.mjs +122 -62
  63. package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
  64. package/scripts/projects/archive-done-items.mjs +410 -0
  65. package/scripts/projects/reorder-queue-item.mjs +334 -76
  66. package/scripts/refine/_refine-helpers.mjs +21 -9
  67. package/scripts/refine/verify.mjs +31 -13
  68. package/skills/dev-loop/SKILL.md +9 -5
  69. package/skills/docs/merge-preconditions.md +15 -0
  70. package/skills/docs/pr-lifecycle-contract.md +1 -0
@@ -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
- 1. Run the deterministic startup resolver to produce the authoritative state bundle: `npx dev-loops loop startup --issue <n>` for issues, or `npx dev-loops loop startup --pr <n>` for PRs.
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 queue board items",
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.5"
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.5",
81
+ "version": "0.2.7",
82
82
  "files": [
83
83
  "cli/",
84
84
  "lib/",
@@ -1,6 +1,8 @@
1
1
  // Re-export from shared library (Phase 2, issue #548)
2
2
  export {
3
+ parseCliTokens,
3
4
  requireOptionValue,
5
+ requireTokenValue,
4
6
  parsePositiveInteger,
5
7
  parseNonNegativeInteger,
6
8
  parsePrNumber,
@@ -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
- return [
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 { requireOptionValue } from "../_cli-primitives.mjs";
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 args = [...argv];
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
- while (args.length > 0) {
62
- const token = args.shift();
63
-
64
- if (token === "--help" || token === "-h") {
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
- if (token === "--root") {
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 { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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 args = [...argv];
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
- while (args.length > 0) {
61
- const token = args.shift();
62
- if (token === "--help" || token === "-h") {
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 === "--input") {
67
- options.inputPath = requireOptionValue(args, "--input");
84
+ if (token.name === "input") {
85
+ options.inputPath = requireTokenValue(token);
68
86
  continue;
69
87
  }
70
- if (token === "--output") {
71
- options.outputPath = requireOptionValue(args, "--output");
88
+ if (token.name === "output") {
89
+ options.outputPath = requireTokenValue(token);
72
90
  continue;
73
91
  }
74
- if (token === "--repo") {
75
- options.repo = requireOptionValue(args, "--repo");
92
+ if (token.name === "repo") {
93
+ options.repo = requireTokenValue(token);
76
94
  continue;
77
95
  }
78
- if (token === "--pr") {
79
- options.pr = parsePrNumber(requireOptionValue(args, "--pr"));
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, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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 args = [...argv];
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
- while (args.length > 0) {
94
- const token = args.shift();
95
- if (token === "--help" || token === "-h") {
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 === "--repo") {
100
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
116
+ if (token.name === "repo") {
117
+ options.repo = requireTokenValue(token, parseError).trim();
101
118
  continue;
102
119
  }
103
- if (token === "--pr") {
104
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
120
+ if (token.name === "pr") {
121
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
105
122
  continue;
106
123
  }
107
- if (token === "--require-before-merge") {
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, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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 args = [...argv];
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
- while (args.length > 0) {
85
- const token = args.shift();
86
- if (token === "--help" || token === "-h") {
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 === "--repo") {
91
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
102
+ if (token.name === "repo") {
103
+ options.repo = requireTokenValue(token, parseError).trim();
92
104
  continue;
93
105
  }
94
- if (token === "--issue") {
95
- options.issue = parseIssueNumber(requireOptionValue(args, "--issue", parseError), parseError);
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, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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
- while (args.length > 0) {
80
- const token = args.shift();
81
- if (token === "--help" || token === "-h") {
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 === "--repo") {
86
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
106
+ if (token.name === "repo") {
107
+ options.repo = requireTokenValue(token, parseError).trim();
87
108
  continue;
88
109
  }
89
- if (token === "--issue") {
90
- options.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "Issue number", parseError);
110
+ if (token.name === "issue") {
111
+ options.issue = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
91
112
  continue;
92
113
  }
93
- if (token === "--child") {
94
- options.child = parsePositiveInteger(requireOptionValue(args, "--child", parseError), "Issue number", parseError);
114
+ if (token.name === "child") {
115
+ options.child = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
95
116
  continue;
96
117
  }
97
- if (token === "--order") {
98
- options.order = parseIssueList(requireOptionValue(args, "--order", parseError));
118
+ if (token.name === "order") {
119
+ options.order = parseIssueList(requireTokenValue(token, parseError));
99
120
  continue;
100
121
  }
101
- if (token === "--expected") {
102
- options.expected = parseIssueList(requireOptionValue(args, "--expected", parseError));
122
+ if (token.name === "expected") {
123
+ options.expected = parseIssueList(requireTokenValue(token, parseError));
103
124
  continue;
104
125
  }
105
- if (token === "--ordered") {
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 { parsePositiveInteger, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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 args = [...argv];
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
- while (args.length > 0) {
100
- const token = args.shift();
101
- if (token === "--help" || token === "-h") {
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 === "--repo") {
109
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
120
+ if (token.name === "repo") {
121
+ options.repo = requireTokenValue(token, parseError).trim();
110
122
  continue;
111
123
  }
112
- if (token === "--pr") {
113
- options.pr = parsePositiveInteger(requireOptionValue(args, "--pr", parseError), "--pr", parseError);
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, requireOptionValue, runChild } from "../_cli-primitives.mjs";
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 args = [...argv], opts = { help: false, repo: undefined, pr: undefined };
13
- while (args.length > 0) {
14
- const token = args.shift();
15
- if (token === "--help" || token === "-h") { opts.help = true; return opts; }
16
- if (token === "--repo") { opts.repo = requireOptionValue(args, "--repo", parseError).trim(); continue; }
17
- if (token === "--pr") { opts.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError); continue; }
18
- throw parseError(`Unknown argument: ${token}`);
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)}`);