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
@@ -3,6 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import process from "node:process";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "node:util";
6
7
  import {
7
8
  STEERING_KIND,
8
9
  STEERING_RESULT,
@@ -31,7 +32,7 @@ import {
31
32
  withStateFileLock,
32
33
  } from "./_steering-state-file.mjs";
33
34
  import { formatCliError } from "../_core-helpers.mjs";
34
- import { requireOptionValue as readSharedOptionValue } from "../_cli-primitives.mjs";
35
+ import { requireTokenValue as readSharedTokenValue } from "../_cli-primitives.mjs";
35
36
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
36
37
  const SUBMIT_USAGE = `Usage:
37
38
  steer-loop.mjs submit --repo <owner/name> --pr <number>
@@ -112,10 +113,9 @@ function runIdMismatchError(persistedRunId, requestedRunId) {
112
113
  `run-id mismatch: --state-file contains run ${JSON.stringify(persistedRunId)} but --run-id is ${JSON.stringify(requestedRunId)}. Use the correct --run-id or point --state-file at the right file.`
113
114
  );
114
115
  }
115
- function readRequiredOptionValue(args, flag, usage, { allowFlagLike = false } = {}) {
116
- return readSharedOptionValue(
117
- args,
118
- flag,
116
+ function readRequiredOptionValue(token, usage, { allowFlagLike = false } = {}) {
117
+ return readSharedTokenValue(
118
+ token,
119
119
  (message) => usageError(message, usage),
120
120
  { flagPattern: allowFlagLike ? /$^/u : /^--/u },
121
121
  );
@@ -139,7 +139,6 @@ function parsePositiveIntegerOption(raw, flag, usage) {
139
139
  return Number(raw);
140
140
  }
141
141
  export function parseSubmitCliArgs(argv) {
142
- const args = [...argv];
143
142
  const options = {
144
143
  help: false,
145
144
  repo: undefined,
@@ -156,48 +155,74 @@ export function parseSubmitCliArgs(argv) {
156
155
  copilotInputPath: undefined,
157
156
  reviewerInputPath: undefined,
158
157
  };
159
- while (args.length > 0) {
160
- const token = args.shift();
161
- if (token === "--help" || token === "-h") {
158
+ const { tokens } = parseArgs({
159
+ args: [...argv],
160
+ options: {
161
+ help: { type: "boolean", short: "h" },
162
+ "run-id": { type: "string" },
163
+ repo: { type: "string" },
164
+ pr: { type: "string" },
165
+ kind: { type: "string" },
166
+ directive: { type: "string" },
167
+ seq: { type: "string" },
168
+ "state-file": { type: "string" },
169
+ "loop-state": { type: "string" },
170
+ "apply-mode": { type: "string" },
171
+ "event-id": { type: "string" },
172
+ "copilot-input": { type: "string" },
173
+ "reviewer-input": { type: "string" },
174
+ },
175
+ allowPositionals: true,
176
+ strict: false,
177
+ tokens: true,
178
+ });
179
+ for (const token of tokens) {
180
+ if (token.kind === "positional") {
181
+ throw usageError(`Unknown argument: ${token.value}`, SUBMIT_USAGE);
182
+ }
183
+ if (token.kind !== "option") {
184
+ continue;
185
+ }
186
+ if (token.name === "help") {
162
187
  options.help = true;
163
188
  return options;
164
189
  }
165
- if (token === "--run-id") {
166
- options.runId = readRequiredOptionValue(args, "--run-id", SUBMIT_USAGE).trim();
190
+ if (token.name === "run-id") {
191
+ options.runId = readRequiredOptionValue(token, SUBMIT_USAGE).trim();
167
192
  validateSafeRunId(options.runId, SUBMIT_USAGE);
168
193
  continue;
169
194
  }
170
- if (token === "--repo") {
171
- options.repo = readRequiredOptionValue(args, "--repo", SUBMIT_USAGE).trim();
195
+ if (token.name === "repo") {
196
+ options.repo = readRequiredOptionValue(token, SUBMIT_USAGE).trim();
172
197
  parseRepoSlugOption(options.repo, SUBMIT_USAGE);
173
198
  continue;
174
199
  }
175
- if (token === "--pr") {
176
- options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", SUBMIT_USAGE), "--pr", SUBMIT_USAGE);
200
+ if (token.name === "pr") {
201
+ options.pr = parsePositiveIntegerOption(readRequiredOptionValue(token, SUBMIT_USAGE), "--pr", SUBMIT_USAGE);
177
202
  continue;
178
203
  }
179
- if (token === "--kind") {
180
- const val = readRequiredOptionValue(args, "--kind", SUBMIT_USAGE);
204
+ if (token.name === "kind") {
205
+ const val = readRequiredOptionValue(token, SUBMIT_USAGE);
181
206
  if (!VALID_KINDS.has(val)) {
182
207
  throw usageError(`--kind must be one of: ${[...VALID_KINDS].join(", ")}`, SUBMIT_USAGE);
183
208
  }
184
209
  options.kind = val;
185
210
  continue;
186
211
  }
187
- if (token === "--directive") {
188
- options.directive = readRequiredOptionValue(args, "--directive", SUBMIT_USAGE, { allowFlagLike: true }).trim();
212
+ if (token.name === "directive") {
213
+ options.directive = readRequiredOptionValue(token, SUBMIT_USAGE, { allowFlagLike: true }).trim();
189
214
  continue;
190
215
  }
191
- if (token === "--seq") {
192
- options.seq = parsePositiveIntegerOption(readRequiredOptionValue(args, "--seq", SUBMIT_USAGE), "--seq", SUBMIT_USAGE);
216
+ if (token.name === "seq") {
217
+ options.seq = parsePositiveIntegerOption(readRequiredOptionValue(token, SUBMIT_USAGE), "--seq", SUBMIT_USAGE);
193
218
  continue;
194
219
  }
195
- if (token === "--state-file") {
196
- options.stateFile = readRequiredOptionValue(args, "--state-file", SUBMIT_USAGE);
220
+ if (token.name === "state-file") {
221
+ options.stateFile = readRequiredOptionValue(token, SUBMIT_USAGE);
197
222
  continue;
198
223
  }
199
- if (token === "--loop-state") {
200
- const val = readRequiredOptionValue(args, "--loop-state", SUBMIT_USAGE);
224
+ if (token.name === "loop-state") {
225
+ const val = readRequiredOptionValue(token, SUBMIT_USAGE);
201
226
  if (!VALID_LOOP_STATES.has(val)) {
202
227
  throw usageError(`--loop-state must be one of: ${[...VALID_LOOP_STATES].join(", ")}`, SUBMIT_USAGE);
203
228
  }
@@ -205,27 +230,27 @@ export function parseSubmitCliArgs(argv) {
205
230
  options.loopStateExplicit = true;
206
231
  continue;
207
232
  }
208
- if (token === "--apply-mode") {
209
- const val = readRequiredOptionValue(args, "--apply-mode", SUBMIT_USAGE);
233
+ if (token.name === "apply-mode") {
234
+ const val = readRequiredOptionValue(token, SUBMIT_USAGE);
210
235
  if (!VALID_APPLY_MODES.has(val)) {
211
236
  throw usageError(`--apply-mode must be one of: ${[...VALID_APPLY_MODES].join(", ")}`, SUBMIT_USAGE);
212
237
  }
213
238
  options.applyMode = val;
214
239
  continue;
215
240
  }
216
- if (token === "--event-id") {
217
- options.eventId = readRequiredOptionValue(args, "--event-id", SUBMIT_USAGE);
241
+ if (token.name === "event-id") {
242
+ options.eventId = readRequiredOptionValue(token, SUBMIT_USAGE);
218
243
  continue;
219
244
  }
220
- if (token === "--copilot-input") {
221
- options.copilotInputPath = readRequiredOptionValue(args, "--copilot-input", SUBMIT_USAGE);
245
+ if (token.name === "copilot-input") {
246
+ options.copilotInputPath = readRequiredOptionValue(token, SUBMIT_USAGE);
222
247
  continue;
223
248
  }
224
- if (token === "--reviewer-input") {
225
- options.reviewerInputPath = readRequiredOptionValue(args, "--reviewer-input", SUBMIT_USAGE);
249
+ if (token.name === "reviewer-input") {
250
+ options.reviewerInputPath = readRequiredOptionValue(token, SUBMIT_USAGE);
226
251
  continue;
227
252
  }
228
- throw usageError(`Unknown argument: ${token}`, SUBMIT_USAGE);
253
+ throw usageError(`Unknown argument: ${token.rawName}`, SUBMIT_USAGE);
229
254
  }
230
255
  if (!options.help) {
231
256
  if ((options.repo === undefined) !== (options.pr === undefined)) {
@@ -250,7 +275,6 @@ export function parseSubmitCliArgs(argv) {
250
275
  return options;
251
276
  }
252
277
  export function parseStatusCliArgs(argv) {
253
- const args = [...argv];
254
278
  const options = {
255
279
  help: false,
256
280
  repo: undefined,
@@ -258,31 +282,49 @@ export function parseStatusCliArgs(argv) {
258
282
  runId: undefined,
259
283
  stateFile: undefined,
260
284
  };
261
- while (args.length > 0) {
262
- const token = args.shift();
263
- if (token === "--help" || token === "-h") {
285
+ const { tokens } = parseArgs({
286
+ args: [...argv],
287
+ options: {
288
+ help: { type: "boolean", short: "h" },
289
+ "run-id": { type: "string" },
290
+ repo: { type: "string" },
291
+ pr: { type: "string" },
292
+ "state-file": { type: "string" },
293
+ },
294
+ allowPositionals: true,
295
+ strict: false,
296
+ tokens: true,
297
+ });
298
+ for (const token of tokens) {
299
+ if (token.kind === "positional") {
300
+ throw usageError(`Unknown argument: ${token.value}`, STATUS_USAGE);
301
+ }
302
+ if (token.kind !== "option") {
303
+ continue;
304
+ }
305
+ if (token.name === "help") {
264
306
  options.help = true;
265
307
  return options;
266
308
  }
267
- if (token === "--run-id") {
268
- options.runId = readRequiredOptionValue(args, "--run-id", STATUS_USAGE).trim();
309
+ if (token.name === "run-id") {
310
+ options.runId = readRequiredOptionValue(token, STATUS_USAGE).trim();
269
311
  validateSafeRunId(options.runId, STATUS_USAGE);
270
312
  continue;
271
313
  }
272
- if (token === "--repo") {
273
- options.repo = readRequiredOptionValue(args, "--repo", STATUS_USAGE).trim();
314
+ if (token.name === "repo") {
315
+ options.repo = readRequiredOptionValue(token, STATUS_USAGE).trim();
274
316
  parseRepoSlugOption(options.repo, STATUS_USAGE);
275
317
  continue;
276
318
  }
277
- if (token === "--pr") {
278
- options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", STATUS_USAGE), "--pr", STATUS_USAGE);
319
+ if (token.name === "pr") {
320
+ options.pr = parsePositiveIntegerOption(readRequiredOptionValue(token, STATUS_USAGE), "--pr", STATUS_USAGE);
279
321
  continue;
280
322
  }
281
- if (token === "--state-file") {
282
- options.stateFile = readRequiredOptionValue(args, "--state-file", STATUS_USAGE);
323
+ if (token.name === "state-file") {
324
+ options.stateFile = readRequiredOptionValue(token, STATUS_USAGE);
283
325
  continue;
284
326
  }
285
- throw usageError(`Unknown argument: ${token}`, STATUS_USAGE);
327
+ throw usageError(`Unknown argument: ${token.rawName}`, STATUS_USAGE);
286
328
  }
287
329
  if (!options.help) {
288
330
  if ((options.repo === undefined) !== (options.pr === undefined)) {
@@ -298,7 +340,6 @@ export function parseStatusCliArgs(argv) {
298
340
  return options;
299
341
  }
300
342
  export function parsePromoteCliArgs(argv) {
301
- const args = [...argv];
302
343
  const options = {
303
344
  help: false,
304
345
  repo: undefined,
@@ -307,39 +348,58 @@ export function parsePromoteCliArgs(argv) {
307
348
  stateFile: undefined,
308
349
  loopState: undefined,
309
350
  };
310
- while (args.length > 0) {
311
- const token = args.shift();
312
- if (token === "--help" || token === "-h") {
351
+ const { tokens } = parseArgs({
352
+ args: [...argv],
353
+ options: {
354
+ help: { type: "boolean", short: "h" },
355
+ "run-id": { type: "string" },
356
+ repo: { type: "string" },
357
+ pr: { type: "string" },
358
+ "state-file": { type: "string" },
359
+ "loop-state": { type: "string" },
360
+ },
361
+ allowPositionals: true,
362
+ strict: false,
363
+ tokens: true,
364
+ });
365
+ for (const token of tokens) {
366
+ if (token.kind === "positional") {
367
+ throw usageError(`Unknown argument: ${token.value}`, PROMOTE_USAGE);
368
+ }
369
+ if (token.kind !== "option") {
370
+ continue;
371
+ }
372
+ if (token.name === "help") {
313
373
  options.help = true;
314
374
  return options;
315
375
  }
316
- if (token === "--run-id") {
317
- options.runId = readRequiredOptionValue(args, "--run-id", PROMOTE_USAGE).trim();
376
+ if (token.name === "run-id") {
377
+ options.runId = readRequiredOptionValue(token, PROMOTE_USAGE).trim();
318
378
  validateSafeRunId(options.runId, PROMOTE_USAGE);
319
379
  continue;
320
380
  }
321
- if (token === "--repo") {
322
- options.repo = readRequiredOptionValue(args, "--repo", PROMOTE_USAGE).trim();
381
+ if (token.name === "repo") {
382
+ options.repo = readRequiredOptionValue(token, PROMOTE_USAGE).trim();
323
383
  parseRepoSlugOption(options.repo, PROMOTE_USAGE);
324
384
  continue;
325
385
  }
326
- if (token === "--pr") {
327
- options.pr = parsePositiveIntegerOption(readRequiredOptionValue(args, "--pr", PROMOTE_USAGE), "--pr", PROMOTE_USAGE);
386
+ if (token.name === "pr") {
387
+ options.pr = parsePositiveIntegerOption(readRequiredOptionValue(token, PROMOTE_USAGE), "--pr", PROMOTE_USAGE);
328
388
  continue;
329
389
  }
330
- if (token === "--state-file") {
331
- options.stateFile = readRequiredOptionValue(args, "--state-file", PROMOTE_USAGE);
390
+ if (token.name === "state-file") {
391
+ options.stateFile = readRequiredOptionValue(token, PROMOTE_USAGE);
332
392
  continue;
333
393
  }
334
- if (token === "--loop-state") {
335
- const val = readRequiredOptionValue(args, "--loop-state", PROMOTE_USAGE);
394
+ if (token.name === "loop-state") {
395
+ const val = readRequiredOptionValue(token, PROMOTE_USAGE);
336
396
  if (!VALID_LOOP_STATES.has(val)) {
337
397
  throw usageError(`--loop-state must be one of: ${[...VALID_LOOP_STATES].join(", ")}`, PROMOTE_USAGE);
338
398
  }
339
399
  options.loopState = val;
340
400
  continue;
341
401
  }
342
- throw usageError(`Unknown argument: ${token}`, PROMOTE_USAGE);
402
+ throw usageError(`Unknown argument: ${token.rawName}`, PROMOTE_USAGE);
343
403
  }
344
404
  if (!options.help) {
345
405
  if ((options.repo === undefined) !== (options.pr === undefined)) {
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { spawn } from "node:child_process";
4
+ import { parseArgs } from "node:util";
4
5
  import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
5
- import { parseIssueNumber, requireOptionValue } from "../_cli-primitives.mjs";
6
+ import { parseIssueNumber, requireTokenValue } from "../_cli-primitives.mjs";
6
7
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
7
8
  import { detectInitialCopilotPrState, LINKED_PR_STATE } from "./detect-initial-copilot-pr-state.mjs";
8
9
  import { enforcePersistentInternalWaitTimeout } from "@dev-loops/core/loop/timeout-policy";
@@ -94,7 +95,6 @@ export async function watchCopilotRunUntilComplete(
94
95
  });
95
96
  }
96
97
  export function parseWatchInitialCopilotPrCliArgs(argv) {
97
- const args = [...argv];
98
98
  const options = {
99
99
  help: false,
100
100
  repo: undefined,
@@ -102,24 +102,40 @@ export function parseWatchInitialCopilotPrCliArgs(argv) {
102
102
  pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
103
103
  timeoutMs: COPILOT_FIRST_DURABLE_WAIT_TIMEOUT_MS,
104
104
  };
105
- while (args.length > 0) {
106
- const token = args.shift();
107
- if (token === "--help" || token === "-h") {
105
+ const { tokens } = parseArgs({
106
+ args: [...argv],
107
+ options: {
108
+ help: { type: "boolean", short: "h" },
109
+ repo: { type: "string" },
110
+ issue: { type: "string" },
111
+ },
112
+ allowPositionals: true,
113
+ strict: false,
114
+ tokens: true,
115
+ });
116
+ for (const token of tokens) {
117
+ if (token.kind === "positional") {
118
+ throw parseError(`Unknown argument: ${token.value}`);
119
+ }
120
+ if (token.kind !== "option") {
121
+ continue;
122
+ }
123
+ if (token.name === "help") {
108
124
  options.help = true;
109
125
  return options;
110
126
  }
111
- if (REMOVED_FLAGS.has(token)) {
112
- rejectRemovedFlag(token);
127
+ if (REMOVED_FLAGS.has(token.rawName)) {
128
+ rejectRemovedFlag(token.rawName);
113
129
  }
114
- if (token === "--repo") {
115
- options.repo = requireOptionValue(args, "--repo", parseError).trim();
130
+ if (token.name === "repo") {
131
+ options.repo = requireTokenValue(token, parseError).trim();
116
132
  continue;
117
133
  }
118
- if (token === "--issue") {
119
- options.issue = parseIssueNumber(requireOptionValue(args, "--issue", parseError), parseError);
134
+ if (token.name === "issue") {
135
+ options.issue = parseIssueNumber(requireTokenValue(token, parseError), parseError);
120
136
  continue;
121
137
  }
122
- throw parseError(`Unknown argument: ${token}`);
138
+ throw parseError(`Unknown argument: ${token.rawName}`);
123
139
  }
124
140
  if (options.repo === undefined || options.issue === undefined) {
125
141
  throw parseError("watch-initial-copilot-pr requires both --repo <owner/name> and --issue <number>");
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
3
  import { runChild as _runChild } from "../_cli-primitives.mjs";
4
+ import { parseArgs } from "node:util";
4
5
 
5
6
  const USAGE = `Usage: dev-loops project add --repo <owner/name> --project <number|id> --item <number>
6
7
 
@@ -23,52 +24,68 @@ Exit codes:
23
24
  3 — project, field, column, or issue/PR not found
24
25
  `.trim();
25
26
 
26
- const VALID_ARGS = new Set(["--repo", "--project", "--item", "--status", "--help", "-h"]);
27
+ function parseCliArgs(argv) {
28
+ const parseError = (message) => Object.assign(new Error(message), { usage: USAGE });
29
+ const requireValue = (token, message) => {
30
+ const v = token.value;
31
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
32
+ throw parseError(message);
33
+ }
34
+ return v;
35
+ };
27
36
 
28
- function parseArgs(argv) {
29
37
  const args = {};
30
- for (let i = 0; i < argv.length; i++) {
31
- const arg = argv[i];
32
- if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
33
- throw Object.assign(
34
- new Error(`Unknown flag: ${arg}`),
35
- { code: "INVALID_ARGS", usage: USAGE },
36
- );
38
+ const { tokens } = parseArgs({
39
+ args: [...argv],
40
+ options: {
41
+ repo: { type: "string" },
42
+ project: { type: "string" },
43
+ item: { type: "string" },
44
+ status: { type: "string" },
45
+ help: { type: "boolean", short: "h" },
46
+ },
47
+ allowPositionals: true,
48
+ strict: false,
49
+ tokens: true,
50
+ });
51
+
52
+ for (const token of tokens) {
53
+ if (token.kind === "positional") {
54
+ throw Object.assign(new Error(`Unexpected argument: ${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
37
55
  }
38
- if (arg === "--repo") {
39
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
40
- throw Object.assign(new Error("--repo requires a value (owner/name)"), { code: "INVALID_REPO" });
41
- }
42
- args.repo = argv[++i];
43
- } else if (arg === "--project") {
44
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
45
- throw Object.assign(new Error("--project requires a value (number or node ID)"), { code: "INVALID_PROJECT" });
46
- }
47
- args.project = argv[++i];
48
- } else if (arg === "--item") {
49
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
50
- throw Object.assign(new Error("--item requires a value (number)"), { code: "INVALID_ITEM" });
51
- }
52
- const val = Number(argv[++i]);
53
- if (!Number.isInteger(val) || val < 1) {
54
- throw Object.assign(
55
- new Error(`--item must be a positive integer, got "${argv[i]}"`),
56
- { code: "INVALID_ITEM" },
57
- );
58
- }
59
- args.item = val;
60
- } else if (arg === "--status") {
61
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
62
- throw Object.assign(new Error("--status requires a value"), { code: "INVALID_STATUS" });
56
+ if (token.kind !== "option") {
57
+ continue;
58
+ }
59
+ switch (token.name) {
60
+ case "help":
61
+ if (token.value !== undefined) {
62
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
63
+ }
64
+ args.help = true;
65
+ break;
66
+ case "repo":
67
+ args.repo = requireValue(token, "--repo requires a value (owner/name)");
68
+ break;
69
+ case "project":
70
+ args.project = requireValue(token, "--project requires a value (number or node ID)");
71
+ break;
72
+ case "item": {
73
+ const raw = requireValue(token, "--item requires a value (number)");
74
+ const val = Number(raw);
75
+ if (!Number.isInteger(val) || val < 1) {
76
+ throw Object.assign(
77
+ new Error(`--item must be a positive integer, got "${raw}"`),
78
+ { code: "INVALID_ITEM" },
79
+ );
80
+ }
81
+ args.item = val;
82
+ break;
63
83
  }
64
- args.status = argv[++i];
65
- } else if (arg === "--help" || arg === "-h") {
66
- args.help = true;
67
- } else {
68
- throw Object.assign(
69
- new Error(`Unexpected argument: ${arg}`),
70
- { code: "INVALID_ARGS", usage: USAGE },
71
- );
84
+ case "status":
85
+ args.status = requireValue(token, "--status requires a value");
86
+ break;
87
+ default:
88
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
72
89
  }
73
90
  }
74
91
  return args;
@@ -499,7 +516,7 @@ async function main(args, { env = process.env, runChild } = {}) {
499
516
  async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
500
517
  let args;
501
518
  try {
502
- args = parseArgs(argv);
519
+ args = parseCliArgs(argv);
503
520
  } catch (err) {
504
521
  stderr.write(`${formatCliError(err)}\n`);
505
522
  process.exitCode = 1;