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,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { parsePrNumber, requireOptionValue } from "../_cli-primitives.mjs";
4
+ import { parseArgs } from "node:util";
5
+ import { parsePrNumber, requireTokenValue } from "../_cli-primitives.mjs";
5
6
  import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
6
7
  const USAGE = `Usage: write-gate-findings-log.mjs --repo <owner/name> --pr <number> --gate <draft_gate|pre_approval_gate> --head-sha <sha> --verdict <clean|findings_present|blocked> --findings <json> [--tmp-root <path>]
7
8
  Write a durable <gate>-<headSha>.json log under deterministic tmp/ paths.
@@ -89,7 +90,22 @@ function parseFindingsJson(raw) {
89
90
  });
90
91
  }
91
92
  export function parseWriteGateFindingsLogCliArgs(argv) {
92
- const args = [...argv];
93
+ const { tokens } = parseArgs({
94
+ args: [...argv],
95
+ options: {
96
+ help: { type: "boolean", short: "h" },
97
+ repo: { type: "string" },
98
+ pr: { type: "string" },
99
+ gate: { type: "string" },
100
+ "head-sha": { type: "string" },
101
+ verdict: { type: "string" },
102
+ findings: { type: "string" },
103
+ "tmp-root": { type: "string" },
104
+ },
105
+ allowPositionals: true,
106
+ strict: false,
107
+ tokens: true,
108
+ });
93
109
  const options = {
94
110
  repo: undefined,
95
111
  pr: undefined,
@@ -99,46 +115,51 @@ export function parseWriteGateFindingsLogCliArgs(argv) {
99
115
  findings: undefined,
100
116
  tmpRoot: "tmp",
101
117
  };
102
- while (args.length > 0) {
103
- const token = args.shift();
104
- if (token === "--help" || token === "-h") {
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") {
105
126
  return { help: true };
106
127
  }
107
- if (token === "--repo") {
108
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
128
+ if (token.name === "repo") {
129
+ options.repo = requireTokenValue(token, parseError).trim();
109
130
  continue;
110
131
  }
111
- if (token === "--pr") {
112
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
132
+ if (token.name === "pr") {
133
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
113
134
  continue;
114
135
  }
115
- if (token === "--gate") {
116
- const gate = normalizeGate(requireOptionValue(args, "--gate", parseError));
136
+ if (token.name === "gate") {
137
+ const gate = normalizeGate(requireTokenValue(token, parseError));
117
138
  if (!gate) throw parseError("--gate must be draft_gate or pre_approval_gate");
118
139
  options.gate = gate;
119
140
  continue;
120
141
  }
121
- if (token === "--head-sha") {
122
- const sha = normalizeHeadSha(requireOptionValue(args, "--head-sha", parseError));
142
+ if (token.name === "head-sha") {
143
+ const sha = normalizeHeadSha(requireTokenValue(token, parseError));
123
144
  if (!sha) throw parseError("--head-sha must be a 7-64 character hex SHA");
124
145
  options.headSha = sha;
125
146
  continue;
126
147
  }
127
- if (token === "--verdict") {
128
- const verdict = normalizeVerdict(requireOptionValue(args, "--verdict", parseError));
148
+ if (token.name === "verdict") {
149
+ const verdict = normalizeVerdict(requireTokenValue(token, parseError));
129
150
  if (!verdict) throw parseError("--verdict must be clean, findings_present, or blocked");
130
151
  options.verdict = verdict;
131
152
  continue;
132
153
  }
133
- if (token === "--findings") {
134
- options.findings = requireOptionValue(args, "--findings", parseError);
154
+ if (token.name === "findings") {
155
+ options.findings = requireTokenValue(token, parseError);
135
156
  continue;
136
157
  }
137
- if (token === "--tmp-root") {
138
- options.tmpRoot = requireOptionValue(args, "--tmp-root", parseError).trim();
158
+ if (token.name === "tmp-root") {
159
+ options.tmpRoot = requireTokenValue(token, parseError).trim();
139
160
  continue;
140
161
  }
141
- throw parseError(`Unknown argument: ${token}`);
162
+ throw parseError(`Unknown argument: ${token.rawName}`);
142
163
  }
143
164
  const missing = ["repo", "pr", "gate", "headSha", "verdict", "findings"]
144
165
  .filter(k => options[k] === undefined);
@@ -147,7 +168,7 @@ export function parseWriteGateFindingsLogCliArgs(argv) {
147
168
  }
148
169
  return options;
149
170
  }
150
- function buildLogPath({ repo, pr, gate, headSha, tmpRoot }) {
171
+ export function buildLogPath({ repo, pr, gate, headSha, tmpRoot }) {
151
172
  const parts = repo.split("/");
152
173
  if (parts.length !== 2 || parts.some(p => p.length === 0)) {
153
174
  throw new Error(`--repo must be in owner/name format, got: ${JSON.stringify(repo)}`);
@@ -18,10 +18,11 @@ import { readFile } from "node:fs/promises";
18
18
  import { detectRepoSlug } from "@dev-loops/core/github/repo-slug";
19
19
  import path from "node:path";
20
20
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
21
- import { requireOptionValue } from "../_cli-primitives.mjs";
21
+ import { requireTokenValue } from "../_cli-primitives.mjs";
22
22
  import { buildDevLoopHandoffEnvelope } from "@dev-loops/core/loop/handoff-envelope";
23
23
  import { loadDevLoopConfig } from "@dev-loops/core/config";
24
24
  import { createPiAdapter } from "@dev-loops/core/harness";
25
+ import { parseArgs } from "node:util";
25
26
 
26
27
  const USAGE = `Usage: build-handoff-envelope.mjs --input <path>
27
28
  Build a deterministic handoff envelope from startup resolver output and settings.
@@ -56,7 +57,6 @@ function parseFlagJson(raw, flagName, parseErrorFn) {
56
57
  }
57
58
 
58
59
  export function parseBuildHandoffEnvelopeCliArgs(argv) {
59
- const args = [...argv];
60
60
  const options = {
61
61
  help: false,
62
62
  inputPath: undefined,
@@ -65,29 +65,47 @@ export function parseBuildHandoffEnvelopeCliArgs(argv) {
65
65
  repo: undefined,
66
66
  };
67
67
 
68
- while (args.length > 0) {
69
- const token = args.shift();
70
- if (token === "--help" || token === "-h") {
68
+ const { tokens } = parseArgs({
69
+ args: [...argv],
70
+ options: {
71
+ help: { type: "boolean", short: "h" },
72
+ input: { type: "string" },
73
+ "gate-state": { type: "string" },
74
+ overrides: { type: "string" },
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") {
71
89
  options.help = true;
72
90
  return options;
73
91
  }
74
- if (token === "--input") {
75
- options.inputPath = requireOptionValue(args, "--input", parseError);
92
+ if (token.name === "input") {
93
+ options.inputPath = requireTokenValue(token, parseError);
76
94
  continue;
77
95
  }
78
- if (token === "--gate-state") {
79
- options.gateState = requireOptionValue(args, "--gate-state", parseError);
96
+ if (token.name === "gate-state") {
97
+ options.gateState = requireTokenValue(token, parseError);
80
98
  continue;
81
99
  }
82
- if (token === "--overrides") {
83
- options.overrides = requireOptionValue(args, "--overrides", parseError);
100
+ if (token.name === "overrides") {
101
+ options.overrides = requireTokenValue(token, parseError);
84
102
  continue;
85
103
  }
86
- if (token === "--repo") {
87
- options.repo = requireOptionValue(args, "--repo", parseError);
104
+ if (token.name === "repo") {
105
+ options.repo = requireTokenValue(token, parseError);
88
106
  continue;
89
107
  }
90
- throw parseError(`Unknown argument: ${token}`);
108
+ throw parseError(`Unknown argument: ${token.rawName}`);
91
109
  }
92
110
 
93
111
  if (!options.inputPath) {
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { access, open, readFile, readdir } from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { requireOptionValue } from "../_cli-primitives.mjs";
6
+ import { requireTokenValue } from "../_cli-primitives.mjs";
7
7
  import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
8
8
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
9
9
  import { autoDetectSnapshot } from "./detect-copilot-loop-state.mjs";
@@ -14,6 +14,7 @@ import {
14
14
  } from "./_handoff-contract.mjs";
15
15
  import { interpretLoopState, summarizeLoopInterpretation } from "@dev-loops/core/loop/copilot-loop-state";
16
16
  import { listOpenPrs } from "./_loop-pr-aggregation.mjs";
17
+ import { parseArgs } from "node:util";
17
18
  const USAGE = `Usage: conductor-monitor.mjs --repo <owner/name> [--auto-resume]
18
19
  Aggregate Copilot-loop status across all open PRs in one repo.
19
20
  Required:
@@ -113,27 +114,42 @@ const MANUAL_REASON = {
113
114
  HANDOFF_CONTRACT_MISMATCH: "handoff_contract_mismatch",
114
115
  };
115
116
  function parseCliArgs(argv) {
116
- const args = [...argv];
117
117
  const options = {
118
118
  help: false,
119
119
  repo: undefined,
120
120
  autoResume: false,
121
121
  };
122
- while (args.length > 0) {
123
- const token = args.shift();
124
- if (token === "--help" || token === "-h") {
122
+ const { tokens } = parseArgs({
123
+ args: [...argv],
124
+ options: {
125
+ help: { type: "boolean", short: "h" },
126
+ repo: { type: "string" },
127
+ "auto-resume": { type: "boolean" },
128
+ },
129
+ allowPositionals: true,
130
+ strict: false,
131
+ tokens: true,
132
+ });
133
+ for (const token of tokens) {
134
+ if (token.kind === "positional") {
135
+ throw parseError(`Unknown argument: ${token.value}`);
136
+ }
137
+ if (token.kind !== "option") {
138
+ continue;
139
+ }
140
+ if (token.name === "help") {
125
141
  options.help = true;
126
142
  return options;
127
143
  }
128
- if (token === "--repo") {
129
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
144
+ if (token.name === "repo") {
145
+ options.repo = requireTokenValue(token, parseError).trim();
130
146
  continue;
131
147
  }
132
- if (token === "--auto-resume") {
148
+ if (token.name === "auto-resume") {
133
149
  options.autoResume = true;
134
150
  continue;
135
151
  }
136
- throw parseError(`Unknown argument: ${token}`);
152
+ throw parseError(`Unknown argument: ${token.rawName}`);
137
153
  }
138
154
  if (options.repo === undefined) {
139
155
  throw parseError("conductor-monitor requires --repo <owner/name>");
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { runConductorCycle } from "./run-conductor-cycle.mjs";
3
3
  import { runConductorMonitor } from "./conductor-monitor.mjs";
4
- import { requireOptionValue } from "../_cli-primitives.mjs";
4
+ import { requireTokenValue } from "../_cli-primitives.mjs";
5
5
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
6
6
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
7
  import {
@@ -12,6 +12,7 @@ import {
12
12
  } from "@dev-loops/core/config";
13
13
  import { readFileSync } from "node:fs";
14
14
  import path from "node:path";
15
+ import { parseArgs } from "node:util";
15
16
  const USAGE = `Usage: conductor.mjs --repo <owner/name> [--auto-resume] [--cycle-only] [--monitor-only] [--require-retrospective]
16
17
  Unified conductor entrypoint for dev-loop lifecycle orchestration.`.trim();
17
18
  const parseError = buildParseError(USAGE);
@@ -43,7 +44,6 @@ function checkRetrospectiveGate(cwd, requireRetrospective) {
43
44
  }
44
45
  }
45
46
  function parseCliArgs(argv) {
46
- const args = [...argv];
47
47
  const options = {
48
48
  help: false,
49
49
  repo: undefined,
@@ -52,33 +52,52 @@ function parseCliArgs(argv) {
52
52
  monitorOnly: false,
53
53
  requireRetrospective: false,
54
54
  };
55
- while (args.length > 0) {
56
- const token = args.shift();
57
- if (token === "--help" || token === "-h") {
55
+ const { tokens } = parseArgs({
56
+ args: [...argv],
57
+ options: {
58
+ help: { type: "boolean", short: "h" },
59
+ repo: { type: "string" },
60
+ "auto-resume": { type: "boolean" },
61
+ "cycle-only": { type: "boolean" },
62
+ "monitor-only": { type: "boolean" },
63
+ "require-retrospective": { type: "boolean" },
64
+ },
65
+ allowPositionals: true,
66
+ strict: false,
67
+ tokens: true,
68
+ });
69
+ for (const token of tokens) {
70
+ if (token.kind === "positional") {
71
+ throw parseError(`Unknown argument: ${token.value}`);
72
+ }
73
+ if (token.kind !== "option") {
74
+ continue;
75
+ }
76
+ if (token.name === "help") {
58
77
  options.help = true;
59
78
  return options;
60
79
  }
61
- if (token === "--repo") {
62
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
80
+ if (token.name === "repo") {
81
+ options.repo = requireTokenValue(token, parseError).trim();
63
82
  continue;
64
83
  }
65
- if (token === "--auto-resume") {
84
+ if (token.name === "auto-resume") {
66
85
  options.autoResume = true;
67
86
  continue;
68
87
  }
69
- if (token === "--cycle-only") {
88
+ if (token.name === "cycle-only") {
70
89
  options.cycleOnly = true;
71
90
  continue;
72
91
  }
73
- if (token === "--monitor-only") {
92
+ if (token.name === "monitor-only") {
74
93
  options.monitorOnly = true;
75
94
  continue;
76
95
  }
77
- if (token === "--require-retrospective") {
96
+ if (token.name === "require-retrospective") {
78
97
  options.requireRetrospective = true;
79
98
  continue;
80
99
  }
81
- throw parseError(`Unknown argument: ${token}`);
100
+ throw parseError(`Unknown argument: ${token.rawName}`);
82
101
  }
83
102
  if (options.repo === undefined) {
84
103
  throw parseError("conductor requires --repo <owner/name>");
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { buildParseError, formatCliError, isCopilotLogin, isDirectCliRun, normalizeTimestamp } from "../_core-helpers.mjs";
3
- import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
3
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
4
4
  import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
5
5
  import { resolveRunId } from "@dev-loops/core/loop/run-context";
6
6
  import path from "node:path";
@@ -16,6 +16,7 @@ import {
16
16
  EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
17
17
  enforceExternalHealthyWaitTimeout,
18
18
  } from "@dev-loops/core/loop/timeout-policy";
19
+ import { parseArgs } from "node:util";
19
20
  import {
20
21
  DEFAULT_POLL_INTERVAL_MS,
21
22
  COPILOT_REVIEW_WAIT_TIMEOUT_MS,
@@ -128,39 +129,55 @@ function rejectRemovedFlag(token) {
128
129
  );
129
130
  }
130
131
  export function parseHandoffCliArgs(argv, { cwd = process.cwd() } = {}) {
131
- const args = [...argv];
132
132
  const options = {
133
133
  help: false,
134
134
  repo: undefined,
135
135
  pr: undefined,
136
136
  watchStatus: undefined,
137
137
  };
138
- while (args.length > 0) {
139
- const token = args.shift();
140
- if (token === "--help" || token === "-h") {
138
+ const { tokens } = parseArgs({
139
+ args: [...argv],
140
+ options: {
141
+ help: { type: "boolean", short: "h" },
142
+ repo: { type: "string" },
143
+ pr: { type: "string" },
144
+ "watch-status": { type: "string" },
145
+ },
146
+ allowPositionals: true,
147
+ strict: false,
148
+ tokens: true,
149
+ });
150
+ for (const token of tokens) {
151
+ if (token.kind === "positional") {
152
+ throw parseError(`Unknown argument: ${token.value}`);
153
+ }
154
+ if (token.kind !== "option") {
155
+ continue;
156
+ }
157
+ if (token.name === "help") {
141
158
  options.help = true;
142
159
  return options;
143
160
  }
144
- if (REMOVED_FLAGS.has(token)) {
145
- rejectRemovedFlag(token);
161
+ if (REMOVED_FLAGS.has(token.rawName)) {
162
+ rejectRemovedFlag(token.rawName);
146
163
  }
147
- if (token === "--repo") {
148
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
164
+ if (token.name === "repo") {
165
+ options.repo = requireTokenValue(token, parseError).trim();
149
166
  continue;
150
167
  }
151
- if (token === "--pr") {
152
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
168
+ if (token.name === "pr") {
169
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
153
170
  continue;
154
171
  }
155
- if (token === "--watch-status") {
156
- const watchStatus = requireOptionValue(args, "--watch-status", parseError).trim().toLowerCase();
172
+ if (token.name === "watch-status") {
173
+ const watchStatus = requireTokenValue(token, parseError).trim().toLowerCase();
157
174
  if (!VALID_WATCH_STATUSES.has(watchStatus)) {
158
175
  throw parseError(`--watch-status must be one of: ${[...VALID_WATCH_STATUSES].join(", ")}`);
159
176
  }
160
177
  options.watchStatus = watchStatus;
161
178
  continue;
162
179
  }
163
- throw parseError(`Unknown argument: ${token}`);
180
+ throw parseError(`Unknown argument: ${token.rawName}`);
164
181
  }
165
182
  if (options.pr === undefined) {
166
183
  throw parseError("copilot-pr-handoff requires --pr <number>");
@@ -15,9 +15,10 @@ import { readFile } from "node:fs/promises";
15
15
  import path from "node:path";
16
16
  import { fileURLToPath } from "node:url";
17
17
  import { execFileSync } from "node:child_process";
18
+ import { parseArgs } from "node:util";
18
19
 
19
20
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
20
- import { requireOptionValue } from "../_cli-primitives.mjs";
21
+ import { requireTokenValue } from "../_cli-primitives.mjs";
21
22
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
22
23
  import { DebtSignalSchema } from "@dev-loops/core/debt/signal";
23
24
  import { clusterSignalsEnriched } from "@dev-loops/core/debt/cluster";
@@ -150,28 +151,44 @@ function buildReport(signalsCount, findingsCount, results) {
150
151
  // ============================================================================
151
152
 
152
153
  export async function runCli(argv) {
153
- const args = [...argv];
154
154
  const options = { input: undefined, repo: undefined, dryRun: false, help: false };
155
155
 
156
- while (args.length > 0) {
157
- const token = args.shift();
158
- if (token === "--help" || token === "-h") {
156
+ const { tokens } = parseArgs({
157
+ args: [...argv],
158
+ options: {
159
+ help: { type: "boolean", short: "h" },
160
+ input: { type: "string" },
161
+ repo: { type: "string" },
162
+ "dry-run": { type: "boolean" },
163
+ },
164
+ allowPositionals: true,
165
+ strict: false,
166
+ tokens: true,
167
+ });
168
+ for (const token of tokens) {
169
+ if (token.kind === "positional") {
170
+ throw parseError(`Unknown flag: ${token.value}`);
171
+ }
172
+ if (token.kind !== "option") {
173
+ continue;
174
+ }
175
+ if (token.name === "help") {
159
176
  options.help = true;
160
177
  break;
161
178
  }
162
- if (token === "--input") {
163
- options.input = requireOptionValue(args, "--input", parseError);
179
+ if (token.name === "input") {
180
+ options.input = requireTokenValue(token, parseError);
164
181
  continue;
165
182
  }
166
- if (token === "--repo") {
167
- options.repo = requireOptionValue(args, "--repo", parseError);
183
+ if (token.name === "repo") {
184
+ options.repo = requireTokenValue(token, parseError);
168
185
  continue;
169
186
  }
170
- if (token === "--dry-run") {
187
+ if (token.name === "dry-run") {
171
188
  options.dryRun = true;
172
189
  continue;
173
190
  }
174
- throw parseError(`Unknown flag: ${token}`);
191
+ throw parseError(`Unknown flag: ${token.rawName}`);
175
192
  }
176
193
 
177
194
  if (options.help) {
@@ -1,12 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
3
  import process from "node:process";
4
- function parseArgs() {
5
- const args = process.argv.slice(2);
6
- const opts = { base: null, head: null };
7
- for (let i = 0; i < args.length; i++) {
8
- if (args[i] === "--help" || args[i] === "-h") {
9
- process.stdout.write(`Usage: detect-change-scope.mjs [--base <ref>] [--head <ref>]
4
+ import { parseArgs } from "node:util";
5
+
6
+ const USAGE = `Usage: detect-change-scope.mjs [--base <ref>] [--head <ref>]
10
7
  Detect change scope from git diff for light-mode eligibility.
11
8
  Options:
12
9
  --base <ref> Override base ref (default: HEAD~1)
@@ -15,11 +12,39 @@ Options:
15
12
  Exit codes:
16
13
  0 Success
17
14
  1 Error
18
- `);
19
- process.exit(0);
15
+ `;
16
+
17
+ function parseCliArgs(argv) {
18
+ const { tokens } = parseArgs({
19
+ args: [...argv],
20
+ options: {
21
+ base: { type: "string" },
22
+ head: { type: "string" },
23
+ help: { type: "boolean", short: "h" },
24
+ },
25
+ allowPositionals: true,
26
+ strict: false,
27
+ tokens: true,
28
+ });
29
+
30
+ const opts = { base: null, head: null };
31
+ for (const token of tokens) {
32
+ if (token.kind === "option") {
33
+ if (token.name === "help") {
34
+ if (token.value !== undefined) {
35
+ throw new Error(`unknown argument: ${token.rawName}=${token.value}`);
36
+ }
37
+ process.stdout.write(USAGE);
38
+ process.exit(0);
39
+ }
40
+ if (token.name === "base") {
41
+ opts.base = token.value ?? null;
42
+ continue;
43
+ }
44
+ if (token.name === "head") {
45
+ opts.head = token.value ?? null;
46
+ }
20
47
  }
21
- if (args[i] === "--base" && i + 1 < args.length) opts.base = args[++i];
22
- else if (args[i] === "--head" && i + 1 < args.length) opts.head = args[++i];
23
48
  }
24
49
  return opts;
25
50
  }
@@ -64,7 +89,7 @@ function isEligibleForLightMode(scope, threshold) {
64
89
  return scope.filesChanged <= threshold.maxFiles && scope.linesChanged <= threshold.maxLines;
65
90
  }
66
91
  async function main() {
67
- const opts = parseArgs();
92
+ const opts = parseCliArgs(process.argv.slice(2));
68
93
  const scope = detectScope(opts);
69
94
  let threshold = { maxFiles: 3, maxLines: 200 };
70
95
  let eligible = false;
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { parsePrNumber, requireOptionValue, runChild } from "../_cli-primitives.mjs";
4
+ import { parseArgs } from "node:util";
5
+ import { parsePrNumber, requireTokenValue, runChild } from "../_cli-primitives.mjs";
5
6
  import {
6
7
  buildParseError,
7
8
  formatCliError,
@@ -58,32 +59,48 @@ Exit codes:
58
59
  const VALID_OVERRIDE_STATUSES = new Set(["requested", "already-requested", "unavailable", "none", "failed"]);
59
60
  const parseError = buildParseError(USAGE);
60
61
  export function parseDetectCliArgs(argv) {
61
- const args = [...argv];
62
62
  const options = {
63
63
  help: false,
64
64
  inputPath: undefined,
65
65
  repo: undefined,
66
66
  pr: undefined,
67
67
  };
68
- while (args.length > 0) {
69
- const token = args.shift();
70
- if (token === "--help" || token === "-h") {
68
+ const { tokens } = parseArgs({
69
+ args: [...argv],
70
+ options: {
71
+ help: { type: "boolean", short: "h" },
72
+ input: { type: "string" },
73
+ repo: { type: "string" },
74
+ pr: { type: "string" },
75
+ },
76
+ allowPositionals: true,
77
+ strict: false,
78
+ tokens: true,
79
+ });
80
+ for (const token of tokens) {
81
+ if (token.kind === "positional") {
82
+ throw parseError(`Unknown argument: ${token.value}`);
83
+ }
84
+ if (token.kind !== "option") {
85
+ continue;
86
+ }
87
+ if (token.name === "help") {
71
88
  options.help = true;
72
89
  return options;
73
90
  }
74
- if (token === "--input") {
75
- options.inputPath = requireOptionValue(args, "--input", parseError);
91
+ if (token.name === "input") {
92
+ options.inputPath = requireTokenValue(token, parseError);
76
93
  continue;
77
94
  }
78
- if (token === "--repo") {
79
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
95
+ if (token.name === "repo") {
96
+ options.repo = requireTokenValue(token, parseError).trim();
80
97
  continue;
81
98
  }
82
- if (token === "--pr") {
83
- options.pr = parsePrNumber(requireOptionValue(args, "--pr", parseError), parseError);
99
+ if (token.name === "pr") {
100
+ options.pr = parsePrNumber(requireTokenValue(token, parseError), parseError);
84
101
  continue;
85
102
  }
86
- throw parseError(`Unknown argument: ${token}`);
103
+ throw parseError(`Unknown argument: ${token.rawName}`);
87
104
  }
88
105
  if (options.inputPath !== undefined) {
89
106
  if (options.repo !== undefined || options.pr !== undefined) {