dev-loops 0.2.6 → 0.3.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.
Files changed (92) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/agents/developer.md +1 -0
  4. package/.claude/agents/fixer.md +1 -0
  5. package/.claude/agents/review.md +30 -0
  6. package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
  7. package/.claude/skills/dev-loop/SKILL.md +5 -5
  8. package/.claude/skills/docs/anti-patterns.md +2 -0
  9. package/.claude/skills/docs/copilot-loop-operations.md +2 -2
  10. package/.claude/skills/local-implementation/SKILL.md +17 -3
  11. package/AGENTS.md +1 -1
  12. package/CHANGELOG.md +60 -0
  13. package/agents/dev-loop.agent.md +5 -1
  14. package/agents/developer.agent.md +1 -0
  15. package/agents/fixer.agent.md +1 -0
  16. package/agents/review.agent.md +30 -0
  17. package/cli/index.mjs +42 -7
  18. package/package.json +2 -2
  19. package/scripts/README.md +6 -5
  20. package/scripts/_cli-primitives.mjs +2 -0
  21. package/scripts/_core-helpers.mjs +1 -0
  22. package/scripts/claude/generate-claude-assets.mjs +12 -2
  23. package/scripts/claude/headless-dev-loop.mjs +53 -13
  24. package/scripts/claude/headless-info-smoke.mjs +45 -11
  25. package/scripts/docs/validate-links.mjs +20 -11
  26. package/scripts/github/build-adjacent-bundle.mjs +448 -0
  27. package/scripts/github/capture-review-threads.mjs +32 -14
  28. package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
  29. package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
  30. package/scripts/github/detect-linked-issue-pr.mjs +22 -10
  31. package/scripts/github/manage-sub-issues.mjs +37 -16
  32. package/scripts/github/post-gate-findings.mjs +392 -0
  33. package/scripts/github/probe-copilot-review.mjs +24 -12
  34. package/scripts/github/ready-for-review.mjs +17 -8
  35. package/scripts/github/reconcile-draft-gate.mjs +24 -12
  36. package/scripts/github/reply-resolve-review-threads.mjs +34 -15
  37. package/scripts/github/request-copilot-review.mjs +97 -19
  38. package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
  39. package/scripts/github/stage-reviewer-draft.mjs +32 -14
  40. package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
  41. package/scripts/github/verify-fresh-review-context.mjs +12 -1
  42. package/scripts/github/write-gate-context.mjs +634 -0
  43. package/scripts/github/write-gate-findings-log.mjs +42 -21
  44. package/scripts/loop/build-handoff-envelope.mjs +32 -14
  45. package/scripts/loop/conductor-monitor.mjs +25 -9
  46. package/scripts/loop/conductor.mjs +31 -12
  47. package/scripts/loop/copilot-pr-handoff.mjs +31 -14
  48. package/scripts/loop/debt-remediate.mjs +28 -11
  49. package/scripts/loop/detect-change-scope.mjs +36 -11
  50. package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
  51. package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
  52. package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
  53. package/scripts/loop/detect-internal-only-pr.mjs +31 -13
  54. package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
  55. package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
  56. package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
  57. package/scripts/loop/detect-stale-runner.mjs +32 -14
  58. package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
  59. package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
  60. package/scripts/loop/info.mjs +28 -10
  61. package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
  62. package/scripts/loop/inspect-run.mjs +35 -16
  63. package/scripts/loop/outer-loop.mjs +35 -16
  64. package/scripts/loop/pr-runner-coordination.mjs +31 -12
  65. package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
  66. package/scripts/loop/pre-flight-gate.mjs +25 -9
  67. package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
  68. package/scripts/loop/pre-push-main-guard.mjs +19 -5
  69. package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
  70. package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
  71. package/scripts/loop/run-conductor-cycle.mjs +23 -8
  72. package/scripts/loop/run-queue.mjs +87 -15
  73. package/scripts/loop/run-refinement-audit.mjs +44 -22
  74. package/scripts/loop/run-watch-cycle.mjs +28 -12
  75. package/scripts/loop/steer-loop.mjs +122 -62
  76. package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
  77. package/scripts/projects/add-queue-item.mjs +60 -43
  78. package/scripts/projects/archive-done-items.mjs +506 -0
  79. package/scripts/projects/ensure-queue-board.mjs +65 -64
  80. package/scripts/projects/list-queue-items.mjs +57 -56
  81. package/scripts/projects/move-queue-item.mjs +123 -124
  82. package/scripts/projects/reorder-queue-item.mjs +384 -108
  83. package/scripts/projects/sync-item-status.mjs +198 -0
  84. package/scripts/refine/_refine-helpers.mjs +21 -9
  85. package/scripts/refine/verify.mjs +31 -13
  86. package/skills/copilot-pr-followup/SKILL.md +57 -3
  87. package/skills/dev-loop/SKILL.md +9 -5
  88. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
  89. package/skills/dev-loop/scripts/phase-files.mjs +2 -2
  90. package/skills/docs/anti-patterns.md +2 -0
  91. package/skills/docs/copilot-loop-operations.md +2 -2
  92. package/skills/local-implementation/SKILL.md +17 -3
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
- import { requireOptionValue } from "../_cli-primitives.mjs";
4
+ import { requireTokenValue } from "../_cli-primitives.mjs";
5
5
  import { createPiAdapter } from "@dev-loops/core/harness";
6
+ import { parseArgs } from "node:util";
6
7
  import {
7
8
  isUnderWorktreePath,
8
9
  parseMainWorktreePath,
@@ -30,27 +31,42 @@ Bypass:
30
31
  PI_PREFLIGHT_BYPASS=1 Skip all checks (for development/testing only).`.trim();
31
32
  const parseError = buildParseError(USAGE);
32
33
  export function parsePreFlightGateCliArgs(argv) {
33
- const args = [...argv];
34
34
  const options = {
35
35
  help: false,
36
36
  expectedBranch: undefined,
37
37
  checkSubagents: false,
38
38
  };
39
- while (args.length > 0) {
40
- const token = args.shift();
41
- if (token === "--help" || token === "-h") {
39
+ const { tokens } = parseArgs({
40
+ args: [...argv],
41
+ options: {
42
+ help: { type: "boolean", short: "h" },
43
+ "expected-branch": { type: "string" },
44
+ "check-subagents": { type: "boolean" },
45
+ },
46
+ allowPositionals: true,
47
+ strict: false,
48
+ tokens: true,
49
+ });
50
+ for (const token of tokens) {
51
+ if (token.kind === "positional") {
52
+ throw parseError(`Unknown argument: ${token.value}`);
53
+ }
54
+ if (token.kind !== "option") {
55
+ continue;
56
+ }
57
+ if (token.name === "help") {
42
58
  options.help = true;
43
59
  return options;
44
60
  }
45
- if (token === "--expected-branch") {
46
- options.expectedBranch = requireOptionValue(args, "--expected-branch", parseError, { flagPattern: /^-/u });
61
+ if (token.name === "expected-branch") {
62
+ options.expectedBranch = requireTokenValue(token, parseError, { flagPattern: /^-/u });
47
63
  continue;
48
64
  }
49
- if (token === "--check-subagents") {
65
+ if (token.name === "check-subagents") {
50
66
  options.checkSubagents = true;
51
67
  continue;
52
68
  }
53
- throw parseError(`Unknown argument: ${token}`);
69
+ throw parseError(`Unknown argument: ${token.rawName}`);
54
70
  }
55
71
  return options;
56
72
  }
@@ -7,8 +7,9 @@ import {
7
7
  summarizeGateReviewComments,
8
8
  summarizeGateReviewCommentMarkers,
9
9
  } from "../_core-helpers.mjs";
10
- import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
10
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
11
11
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
12
+ import { parseArgs } from "node:util";
12
13
 
13
14
  const USAGE = `Usage:
14
15
  pre-pr-ready-gate.mjs --repo <owner/name> --pr <number>
@@ -38,14 +39,29 @@ const parseError = buildParseError(USAGE);
38
39
  const PR_VIEW_QUERY = `query($owner:String!, $name:String!, $number:Int!) { repository(owner:$owner, name:$name) { pullRequest(number:$number) { id, isDraft, headRefOid, state } } }`;
39
40
 
40
41
  export function parsePrePrReadyGateCliArgs(argv) {
41
- const args = [...argv];
42
42
  const options = { help: false, repo: undefined, pr: undefined };
43
- while (args.length > 0) {
44
- const token = args.shift();
45
- if (token === "--help" || token === "-h") { options.help = true; return options; }
46
- if (token === "--repo") { options.repo = requireOptionValue(args, "--repo", parseError).trim(); continue; }
47
- if (token === "--pr") { options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError); continue; }
48
- throw parseError(`Unknown argument: ${token}`);
43
+ const { tokens } = parseArgs({
44
+ args: [...argv],
45
+ options: {
46
+ help: { type: "boolean", short: "h" },
47
+ repo: { type: "string" },
48
+ pr: { type: "string" },
49
+ },
50
+ allowPositionals: true,
51
+ strict: false,
52
+ tokens: true,
53
+ });
54
+ for (const token of tokens) {
55
+ if (token.kind === "positional") {
56
+ throw parseError(`Unknown argument: ${token.value}`);
57
+ }
58
+ if (token.kind !== "option") {
59
+ continue;
60
+ }
61
+ if (token.name === "help") { options.help = true; return options; }
62
+ if (token.name === "repo") { options.repo = requireTokenValue(token, parseError).trim(); continue; }
63
+ if (token.name === "pr") { options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError); continue; }
64
+ throw parseError(`Unknown argument: ${token.rawName}`);
49
65
  }
50
66
  if (options.repo === undefined || options.pr === undefined) {
51
67
  throw parseError("pre-pr-ready-gate requires both --repo <owner/name> and --pr <number>");
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createInterface } from "node:readline";
3
3
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
4
+ import { parseArgs } from "node:util";
4
5
 
5
6
  const PI_PREPUSH_BYPASS_VAR = "PI_PREPUSH_BYPASS";
6
7
  const BLOCKED_REFS = ["refs/heads/main"];
@@ -61,11 +62,24 @@ function findBlockedRef(refs) {
61
62
  }
62
63
 
63
64
  export function parsePrePushGuardCliArgs(argv) {
64
- const args = [...argv];
65
- while (args.length > 0) {
66
- const token = args.shift();
67
- if (token === "--help" || token === "-h") return { help: true };
68
- throw parseError(`Unknown argument: ${token}`);
65
+ const { tokens } = parseArgs({
66
+ args: [...argv],
67
+ options: {
68
+ help: { type: "boolean", short: "h" },
69
+ },
70
+ allowPositionals: true,
71
+ strict: false,
72
+ tokens: true,
73
+ });
74
+ for (const token of tokens) {
75
+ if (token.kind === "positional") {
76
+ throw parseError(`Unknown argument: ${token.value}`);
77
+ }
78
+ if (token.kind !== "option") {
79
+ continue;
80
+ }
81
+ if (token.name === "help") return { help: true };
82
+ throw parseError(`Unknown argument: ${token.rawName}`);
69
83
  }
70
84
  return { help: false };
71
85
  }
@@ -1,17 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
3
- import { requireOptionValue, runCommand } from "../_cli-primitives.mjs";
3
+ import { requireTokenValue, runCommand } from "../_cli-primitives.mjs";
4
+ import { parseArgs } from "node:util";
4
5
 
5
6
  const USAGE = `Usage: pre-write-remote-freshness-guard.mjs --branch <name>\nRefresh remote branch state before starting local file writes.`;
6
7
  const parseError = buildParseError(USAGE);
7
8
 
8
9
  export function parseRemoteFreshnessGuardCliArgs(argv) {
9
- const args = [...argv], options = { help: false, branch: undefined };
10
- while (args.length > 0) {
11
- const token = args.shift();
12
- if (token === "--help" || token === "-h") { options.help = true; return options; }
13
- if (token === "--branch") { options.branch = requireOptionValue(args, "--branch", parseError, { flagPattern: /^-/u }); continue; }
14
- throw parseError(`Unknown argument: ${token}`);
10
+ const options = { help: false, branch: undefined };
11
+ const { tokens } = parseArgs({
12
+ args: [...argv],
13
+ options: {
14
+ help: { type: "boolean", short: "h" },
15
+ branch: { type: "string" },
16
+ },
17
+ allowPositionals: true,
18
+ strict: false,
19
+ tokens: true,
20
+ });
21
+ for (const token of tokens) {
22
+ if (token.kind === "positional") {
23
+ throw parseError(`Unknown argument: ${token.value}`);
24
+ }
25
+ if (token.kind !== "option") {
26
+ continue;
27
+ }
28
+ if (token.name === "help") { options.help = true; return options; }
29
+ if (token.name === "branch") { options.branch = requireTokenValue(token, parseError, { flagPattern: /^-/u }); continue; }
30
+ throw parseError(`Unknown argument: ${token.rawName}`);
15
31
  }
16
32
  if (options.branch === undefined) throw parseError("--branch <name> is required");
17
33
  return options;
@@ -4,7 +4,7 @@ import { readFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { resolveAuthoritativeStartupResumeBundle } from "@dev-loops/core/loop/public-dev-loop-routing";
6
6
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
7
- import { requireOptionValue, parsePositiveInteger } from "../_cli-primitives.mjs";
7
+ import { requireTokenValue, parsePositiveInteger } from "../_cli-primitives.mjs";
8
8
  import { execFileSync } from "node:child_process";
9
9
  import {
10
10
  isUnderWorktreePath,
@@ -23,6 +23,7 @@ import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
23
23
  import { isCopilotLogin } from "@dev-loops/core/github/copilot-helpers";
24
24
  import { loadDevLoopConfig, resolveWorkflowConfig } from "@dev-loops/core/config";
25
25
  import { createPiAdapter } from "@dev-loops/core/harness";
26
+ import { parseArgs } from "node:util";
26
27
  const USAGE = `Usage:
27
28
  resolve-dev-loop-startup.mjs --issue <number>
28
29
  resolve-dev-loop-startup.mjs --pr <number>
@@ -96,32 +97,48 @@ const STRATEGY_ASYNC_DISPATCH = {
96
97
  };
97
98
  const parseError = buildParseError(USAGE);
98
99
  export function parseResolveDevLoopStartupCliArgs(argv) {
99
- const args = [...argv];
100
100
  const options = {
101
101
  help: false,
102
102
  inputPath: undefined,
103
103
  issue: undefined,
104
104
  pr: undefined,
105
105
  };
106
- while (args.length > 0) {
107
- const token = args.shift();
108
- if (token === "--help" || token === "-h") {
106
+ const { tokens } = parseArgs({
107
+ args: [...argv],
108
+ options: {
109
+ help: { type: "boolean", short: "h" },
110
+ input: { type: "string" },
111
+ issue: { type: "string" },
112
+ pr: { type: "string" },
113
+ },
114
+ allowPositionals: true,
115
+ strict: false,
116
+ tokens: true,
117
+ });
118
+ for (const token of tokens) {
119
+ if (token.kind === "positional") {
120
+ throw parseError(`Unknown argument: ${token.value}`);
121
+ }
122
+ if (token.kind !== "option") {
123
+ continue;
124
+ }
125
+ if (token.name === "help") {
109
126
  options.help = true;
110
127
  return options;
111
128
  }
112
- if (token === "--input") {
113
- options.inputPath = requireOptionValue(args, "--input", parseError);
129
+ if (token.name === "input") {
130
+ options.inputPath = requireTokenValue(token, parseError);
114
131
  continue;
115
132
  }
116
- if (token === "--issue") {
117
- options.issue = parsePositiveInteger(requireOptionValue(args, "--issue", parseError), "--issue", parseError);
133
+ if (token.name === "issue") {
134
+ options.issue = parsePositiveInteger(requireTokenValue(token, parseError), "--issue", parseError);
118
135
  continue;
119
136
  }
120
- if (token === "--pr") {
121
- options.pr = parsePositiveInteger(requireOptionValue(args, "--pr", parseError), "--pr", parseError);
137
+ if (token.name === "pr") {
138
+ options.pr = parsePositiveInteger(requireTokenValue(token, parseError), "--pr", parseError);
122
139
  continue;
123
140
  }
124
- throw parseError(`Unknown argument: ${token}`);
141
+ throw parseError(`Unknown argument: ${token.rawName}`);
125
142
  }
126
143
  const modeCount = [options.inputPath, options.issue, options.pr].filter(v => v !== undefined).length;
127
144
  if (modeCount > 1) {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { requireOptionValue } from "../_cli-primitives.mjs";
2
+ import { requireTokenValue } from "../_cli-primitives.mjs";
3
3
  import {
4
4
  buildParseError,
5
5
  formatCliError,
@@ -14,6 +14,7 @@ import {
14
14
  buildHandoffContractForConductorAction,
15
15
  } from "./_handoff-contract.mjs";
16
16
  import { listOpenPrs } from "./_loop-pr-aggregation.mjs";
17
+ import { parseArgs } from "node:util";
17
18
  export { listOpenPrs };
18
19
  const USAGE = `Usage: run-conductor-cycle.mjs --repo <owner/name>
19
20
  Poll all open PRs, detect state, and output an ordered action queue.`.trim();
@@ -63,22 +64,36 @@ export function actionRequiresApproval(action, autonomyStopAt = ["merge"]) {
63
64
  }
64
65
  const parseError = buildParseError(USAGE);
65
66
  export function parseCliArgs(argv) {
66
- const args = [...argv];
67
67
  const options = {
68
68
  help: false,
69
69
  repo: undefined,
70
70
  };
71
- while (args.length > 0) {
72
- const token = args.shift();
73
- if (token === "--help" || token === "-h") {
71
+ const { tokens } = parseArgs({
72
+ args: [...argv],
73
+ options: {
74
+ help: { type: "boolean", short: "h" },
75
+ repo: { type: "string" },
76
+ },
77
+ allowPositionals: true,
78
+ strict: false,
79
+ tokens: true,
80
+ });
81
+ for (const token of tokens) {
82
+ if (token.kind === "positional") {
83
+ throw parseError(`Unknown argument: ${token.value}`);
84
+ }
85
+ if (token.kind !== "option") {
86
+ continue;
87
+ }
88
+ if (token.name === "help") {
74
89
  options.help = true;
75
90
  return options;
76
91
  }
77
- if (token === "--repo") {
78
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
92
+ if (token.name === "repo") {
93
+ options.repo = requireTokenValue(token, parseError).trim();
79
94
  continue;
80
95
  }
81
- throw parseError(`Unknown argument: ${token}`);
96
+ throw parseError(`Unknown argument: ${token.rawName}`);
82
97
  }
83
98
  if (options.repo === undefined) {
84
99
  throw parseError("run-conductor-cycle requires --repo <owner/name>");
@@ -14,9 +14,11 @@
14
14
  */
15
15
 
16
16
  import { fileURLToPath } from "node:url";
17
+ import { parseArgs } from "node:util";
17
18
  import { runQueue, DEFAULT_QUEUE_DRIVER_OPTIONS } from "@dev-loops/core/loop/queue-driver";
18
19
  import { computeParallelSchedule } from "@dev-loops/core/loop/queue-parallel";
19
20
  import { readQueue } from "@dev-loops/core/loop/queue-state";
21
+ import { reconcileBoardMembership } from "@dev-loops/core/loop/queue-membership";
20
22
  import { parsePositiveInteger } from "@dev-loops/core/cli/primitives";
21
23
 
22
24
  const REPO_ROOT = fileURLToPath(new URL("../..", import.meta.url));
@@ -27,7 +29,7 @@ const USAGE = `Usage:
27
29
  Run the dev-loop queue driver over entries in .pi/dev-loop-queue.json.
28
30
  Exit codes: 0 success, 1 error`.trim();
29
31
 
30
- function parseArgs(argv) {
32
+ function parseCliArgs(argv) {
31
33
  const args = {
32
34
  repo: null,
33
35
  mergeAuthorized: false,
@@ -37,27 +39,58 @@ function parseArgs(argv) {
37
39
  help: false,
38
40
  };
39
41
 
40
- for (let i = 0; i < argv.length; i++) {
41
- switch (argv[i]) {
42
- case "--repo":
43
- args.repo = argv[++i];
42
+ const { tokens } = parseArgs({
43
+ args: [...argv],
44
+ options: {
45
+ repo: { type: "string" },
46
+ "merge-authorized": { type: "boolean" },
47
+ parallel: { type: "boolean" },
48
+ "redispatch-max-retries": { type: "string" },
49
+ "max-parallel": { type: "string" },
50
+ help: { type: "boolean", short: "h" },
51
+ },
52
+ allowPositionals: true,
53
+ strict: false,
54
+ tokens: true,
55
+ });
56
+
57
+ for (const token of tokens) {
58
+ if (token.kind === "positional") {
59
+ throw new Error(`unknown argument: ${token.value}`);
60
+ }
61
+ if (token.kind !== "option") {
62
+ continue;
63
+ }
64
+ switch (token.name) {
65
+ case "repo":
66
+ args.repo = token.value;
44
67
  break;
45
- case "--merge-authorized":
68
+ case "merge-authorized":
69
+ if (token.value !== undefined) {
70
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
71
+ }
46
72
  args.mergeAuthorized = true;
47
73
  break;
48
- case "--parallel":
74
+ case "parallel":
75
+ if (token.value !== undefined) {
76
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
77
+ }
49
78
  args.parallel = true;
50
79
  break;
51
- case "--redispatch-max-retries":
52
- args.reDispatchMaxRetries = parsePositiveInteger(argv[++i], "--redispatch-max-retries");
80
+ case "redispatch-max-retries":
81
+ args.reDispatchMaxRetries = parsePositiveInteger(token.value, "--redispatch-max-retries");
53
82
  break;
54
- case "--max-parallel":
55
- args.maxParallel = parsePositiveInteger(argv[++i], "--max-parallel");
83
+ case "max-parallel":
84
+ args.maxParallel = parsePositiveInteger(token.value, "--max-parallel");
56
85
  break;
57
- case "--help":
58
- case "-h":
86
+ case "help":
87
+ if (token.value !== undefined) {
88
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
89
+ }
59
90
  args.help = true;
60
91
  break;
92
+ default:
93
+ throw new Error(`unknown argument: ${token.rawName}`);
61
94
  }
62
95
  }
63
96
 
@@ -65,7 +98,7 @@ function parseArgs(argv) {
65
98
  }
66
99
 
67
100
  async function main() {
68
- const args = parseArgs(process.argv.slice(2));
101
+ const args = parseCliArgs(process.argv.slice(2));
69
102
 
70
103
  if (args.help) {
71
104
  console.log(USAGE);
@@ -79,7 +112,46 @@ async function main() {
79
112
 
80
113
  const queue = await readQueue(REPO_ROOT);
81
114
 
82
- if (queue.entries.length === 0) {
115
+ // A configured GitHub Projects board is the authoritative queue MEMBERSHIP
116
+ // source (issue #864): fold its "Next Up" items into the queue before judging
117
+ // emptiness so a populated board with an empty local queue is no longer a
118
+ // silent no-op. Fail-open — a board hiccup falls back to the local queue.
119
+ // reconcileBoardMembership already logs an "added N ... from board Next Up"
120
+ // line to stderr (single source of truth); we deliberately do not duplicate
121
+ // it here to avoid noise for JSON consumers of stdout.
122
+ const membership = await reconcileBoardMembership(REPO_ROOT, args.repo, queue);
123
+
124
+ if (membership.emptiness === "board_empty") {
125
+ // Distinct from the legacy generic "Queue is empty": the board is the
126
+ // membership source and it currently has nothing in Next Up. This branch is
127
+ // only reached for a genuinely empty Next Up (reason == null), never for a
128
+ // resolution failure (which falls through to the local queue below).
129
+ console.log(JSON.stringify({
130
+ ok: true,
131
+ message: "Board configured but Next Up is empty; nothing to run",
132
+ boardConfigured: true,
133
+ reason: null,
134
+ results: [],
135
+ }));
136
+ return;
137
+ }
138
+
139
+ if (membership.emptiness === "board_unavailable") {
140
+ // The board IS configured but Next Up resolution failed (fail-open) and the
141
+ // local queue had nothing to fall back to. Do NOT claim "Next Up is empty";
142
+ // surface the real reason so consumers can distinguish an outage from an
143
+ // intentionally empty board.
144
+ console.log(JSON.stringify({
145
+ ok: true,
146
+ message: `Board configured but unavailable (${membership.reason}); nothing to run`,
147
+ boardConfigured: true,
148
+ reason: membership.reason ?? null,
149
+ results: [],
150
+ }));
151
+ return;
152
+ }
153
+
154
+ if (membership.emptiness === "queue_empty") {
83
155
  console.log(JSON.stringify({ ok: true, message: "Queue is empty", results: [] }));
84
156
  return;
85
157
  }
@@ -2,7 +2,8 @@
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
- import { parsePositiveInteger, requireOptionValue, runCommand } from "../_cli-primitives.mjs";
5
+ import { parsePositiveInteger, requireTokenValue, runCommand } from "../_cli-primitives.mjs";
6
+ import { parseArgs } from "node:util";
6
7
  const DEFAULT_MAX_LINES = 1000;
7
8
  const DEFAULT_DUPLICATE_WINDOW_LINES = 4;
8
9
  const DEFAULT_BRANCH_THRESHOLD = 25;
@@ -43,7 +44,6 @@ const PRIORITY_ORDER = new Map([
43
44
  ["low", 2],
44
45
  ]);
45
46
  export function parseRefinementAuditCliArgs(argv) {
46
- const args = [...argv];
47
47
  const options = {
48
48
  help: false,
49
49
  paths: undefined,
@@ -55,57 +55,79 @@ export function parseRefinementAuditCliArgs(argv) {
55
55
  thinWrapperMaxLines: DEFAULT_THIN_WRAPPER_MAX_LINES,
56
56
  output: undefined,
57
57
  };
58
- while (args.length > 0) {
59
- const token = args.shift();
60
- if (token === "--help" || token === "-h") {
58
+ const { tokens } = parseArgs({
59
+ args: [...argv],
60
+ options: {
61
+ help: { type: "boolean", short: "h" },
62
+ paths: { type: "string" },
63
+ "paths-file": { type: "string" },
64
+ root: { type: "string" },
65
+ "max-lines": { type: "string" },
66
+ "duplicate-window-lines": { type: "string" },
67
+ "branch-threshold": { type: "string" },
68
+ "thin-wrapper-max-lines": { type: "string" },
69
+ output: { type: "string" },
70
+ },
71
+ allowPositionals: true,
72
+ strict: false,
73
+ tokens: true,
74
+ });
75
+ for (const token of tokens) {
76
+ if (token.kind === "positional") {
77
+ throw parseError(`Unknown argument: ${token.value}`);
78
+ }
79
+ if (token.kind !== "option") {
80
+ continue;
81
+ }
82
+ if (token.name === "help") {
61
83
  options.help = true;
62
84
  return options;
63
85
  }
64
- if (token === "--paths") {
65
- options.paths = requireOptionValue(args, "--paths", parseError, { flagPattern: /^-/u });
86
+ if (token.name === "paths") {
87
+ options.paths = requireTokenValue(token, parseError, { flagPattern: /^-/u });
66
88
  continue;
67
89
  }
68
- if (token === "--paths-file") {
69
- options.pathsFile = requireOptionValue(args, "--paths-file", parseError, { flagPattern: /^-/u });
90
+ if (token.name === "paths-file") {
91
+ options.pathsFile = requireTokenValue(token, parseError, { flagPattern: /^-/u });
70
92
  continue;
71
93
  }
72
- if (token === "--root") {
73
- options.root = requireOptionValue(args, "--root", parseError, { flagPattern: /^-/u });
94
+ if (token.name === "root") {
95
+ options.root = requireTokenValue(token, parseError, { flagPattern: /^-/u });
74
96
  continue;
75
97
  }
76
- if (token === "--max-lines") {
77
- options.maxLines = parsePositiveInteger(requireOptionValue(args, "--max-lines", parseError), "--max-lines", parseError);
98
+ if (token.name === "max-lines") {
99
+ options.maxLines = parsePositiveInteger(requireTokenValue(token, parseError), "--max-lines", parseError);
78
100
  continue;
79
101
  }
80
- if (token === "--duplicate-window-lines") {
102
+ if (token.name === "duplicate-window-lines") {
81
103
  options.duplicateWindowLines = parsePositiveInteger(
82
- requireOptionValue(args, "--duplicate-window-lines", parseError),
104
+ requireTokenValue(token, parseError),
83
105
  "--duplicate-window-lines",
84
106
  parseError,
85
107
  );
86
108
  continue;
87
109
  }
88
- if (token === "--branch-threshold") {
110
+ if (token.name === "branch-threshold") {
89
111
  options.branchThreshold = parsePositiveInteger(
90
- requireOptionValue(args, "--branch-threshold", parseError),
112
+ requireTokenValue(token, parseError),
91
113
  "--branch-threshold",
92
114
  parseError,
93
115
  );
94
116
  continue;
95
117
  }
96
- if (token === "--thin-wrapper-max-lines") {
118
+ if (token.name === "thin-wrapper-max-lines") {
97
119
  options.thinWrapperMaxLines = parsePositiveInteger(
98
- requireOptionValue(args, "--thin-wrapper-max-lines", parseError),
120
+ requireTokenValue(token, parseError),
99
121
  "--thin-wrapper-max-lines",
100
122
  parseError,
101
123
  );
102
124
  continue;
103
125
  }
104
- if (token === "--output") {
105
- options.output = requireOptionValue(args, "--output", parseError, { flagPattern: /^-/u });
126
+ if (token.name === "output") {
127
+ options.output = requireTokenValue(token, parseError, { flagPattern: /^-/u });
106
128
  continue;
107
129
  }
108
- throw parseError(`Unknown argument: ${token}`);
130
+ throw parseError(`Unknown argument: ${token.rawName}`);
109
131
  }
110
132
  if (options.paths !== undefined && options.pathsFile !== undefined) {
111
133
  throw parseError("Specify exactly one of --paths or --paths-file");
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
3
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
4
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
5
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
6
6
  import { DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION } from "@dev-loops/core/loop/public-dev-loop-routing";
7
7
  import { watchCopilotReview } from "../github/probe-copilot-review.mjs";
8
8
  import { runHandoff } from "./copilot-pr-handoff.mjs";
9
9
  import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
10
+ import { parseArgs } from "node:util";
10
11
  import {
11
12
  EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
12
13
  enforceExternalHealthyWaitTimeout,
@@ -178,30 +179,45 @@ function buildWatchCycleContractTrace({
178
179
  };
179
180
  }
180
181
  export function parseWatchCycleCliArgs(argv) {
181
- const args = [...argv];
182
182
  const options = {
183
183
  help: false,
184
184
  repo: undefined,
185
185
  pr: undefined,
186
186
  };
187
- while (args.length > 0) {
188
- const token = args.shift();
189
- if (token === "--help" || token === "-h") {
187
+ const { tokens } = parseArgs({
188
+ args: [...argv],
189
+ options: {
190
+ help: { type: "boolean", short: "h" },
191
+ repo: { type: "string" },
192
+ pr: { type: "string" },
193
+ },
194
+ allowPositionals: true,
195
+ strict: false,
196
+ tokens: true,
197
+ });
198
+ for (const token of tokens) {
199
+ if (token.kind === "positional") {
200
+ throw parseError(`Unknown argument: ${token.value}`);
201
+ }
202
+ if (token.kind !== "option") {
203
+ continue;
204
+ }
205
+ if (token.name === "help") {
190
206
  options.help = true;
191
207
  return options;
192
208
  }
193
- if (REMOVED_FLAGS.has(token)) {
194
- rejectRemovedFlag(token);
209
+ if (REMOVED_FLAGS.has(token.rawName)) {
210
+ rejectRemovedFlag(token.rawName);
195
211
  }
196
- if (token === "--repo") {
197
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
212
+ if (token.name === "repo") {
213
+ options.repo = requireTokenValue(token, parseError).trim();
198
214
  continue;
199
215
  }
200
- if (token === "--pr") {
201
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
216
+ if (token.name === "pr") {
217
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
202
218
  continue;
203
219
  }
204
- throw parseError(`Unknown argument: ${token}`);
220
+ throw parseError(`Unknown argument: ${token.rawName}`);
205
221
  }
206
222
  if (options.repo === undefined || options.pr === undefined) {
207
223
  throw parseError("run-watch-cycle requires both --repo <owner/name> and --pr <number>");