dev-loops 0.2.7 → 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 (48) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -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 +43 -0
  13. package/agents/developer.agent.md +1 -0
  14. package/agents/fixer.agent.md +1 -0
  15. package/agents/review.agent.md +30 -0
  16. package/cli/index.mjs +39 -6
  17. package/package.json +2 -2
  18. package/scripts/README.md +6 -5
  19. package/scripts/_core-helpers.mjs +1 -0
  20. package/scripts/claude/headless-dev-loop.mjs +53 -13
  21. package/scripts/claude/headless-info-smoke.mjs +45 -11
  22. package/scripts/github/build-adjacent-bundle.mjs +448 -0
  23. package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
  24. package/scripts/github/detect-checkpoint-evidence.mjs +95 -4
  25. package/scripts/github/post-gate-findings.mjs +392 -0
  26. package/scripts/github/reconcile-draft-gate.mjs +2 -2
  27. package/scripts/github/request-copilot-review.mjs +69 -8
  28. package/scripts/github/upsert-checkpoint-verdict.mjs +597 -15
  29. package/scripts/github/verify-fresh-review-context.mjs +12 -1
  30. package/scripts/github/write-gate-context.mjs +634 -0
  31. package/scripts/github/write-gate-findings-log.mjs +1 -1
  32. package/scripts/loop/detect-change-scope.mjs +36 -11
  33. package/scripts/loop/detect-pr-gate-coordination-state.mjs +30 -18
  34. package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
  35. package/scripts/loop/run-queue.mjs +87 -15
  36. package/scripts/projects/add-queue-item.mjs +60 -43
  37. package/scripts/projects/archive-done-items.mjs +135 -39
  38. package/scripts/projects/ensure-queue-board.mjs +65 -64
  39. package/scripts/projects/list-queue-items.mjs +57 -56
  40. package/scripts/projects/move-queue-item.mjs +123 -124
  41. package/scripts/projects/reorder-queue-item.mjs +62 -44
  42. package/scripts/projects/sync-item-status.mjs +198 -0
  43. package/skills/copilot-pr-followup/SKILL.md +57 -3
  44. package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
  45. package/skills/dev-loop/scripts/phase-files.mjs +2 -2
  46. package/skills/docs/anti-patterns.md +2 -0
  47. package/skills/docs/copilot-loop-operations.md +2 -2
  48. package/skills/local-implementation/SKILL.md +17 -3
@@ -1,17 +1,24 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { parse as parseYaml } from "yaml";
2
5
  import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
3
6
  import { runChild as _runChild } from "../_cli-primitives.mjs";
7
+ import { parseArgs } from "node:util";
4
8
 
5
- const USAGE = `Usage: dev-loops project archive-done --repo <owner/name> --project <number|id> [--older-than <duration>] [--dry-run]
9
+ const USAGE = `Usage: dev-loops project archive-done --repo <owner/name> [--project <number|id>] [--older-than <duration>] [--dry-run]
6
10
 
7
11
  Archive GitHub Projects V2 items whose issue/PR has been closed for at least the
8
12
  given duration. Operator-triggered (no webhooks). Uses archiveProjectV2Item.
9
13
 
10
14
  Options:
11
15
  --repo <owner/name> Required. Repository to scope the project search.
12
- --project <number|id> Required. Project number (integer) or node ID.
16
+ --project <number|id> Project number (integer) or node ID. When omitted,
17
+ resolved from .devloops queue.projectNumber /
18
+ queue.boardTitle.
13
19
  --older-than <duration> Closed-for threshold. Format: <n><unit> where unit is
14
- h (hours), d (days), or w (weeks). Default: 30d.
20
+ h (hours), d (days), or w (weeks). Default resolves
21
+ from .devloops queue.archiveOlderThanDays, else 7d.
15
22
  --dry-run Print the intended archive mutation(s) without executing.
16
23
  --help, -h Show this help.
17
24
 
@@ -27,36 +34,61 @@ Exit codes:
27
34
  3 — project not found
28
35
  `.trim();
29
36
 
30
- const VALID_ARGS = new Set(["--repo", "--project", "--older-than", "--dry-run", "--help", "-h"]);
37
+ function parseCliArgs(argv) {
38
+ const requireValue = (token, message, code) => {
39
+ const v = token.value;
40
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
41
+ throw Object.assign(new Error(message), { code });
42
+ }
43
+ return v;
44
+ };
31
45
 
32
- function parseArgs(argv) {
33
46
  const args = {};
34
- for (let i = 0; i < argv.length; i++) {
35
- const arg = argv[i];
36
- if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
37
- throw Object.assign(new Error(`Unknown flag: ${arg}`), { code: "INVALID_ARGS", usage: USAGE });
47
+ const { tokens } = parseArgs({
48
+ args: [...argv],
49
+ options: {
50
+ repo: { type: "string" },
51
+ project: { type: "string" },
52
+ "older-than": { type: "string" },
53
+ "dry-run": { type: "boolean" },
54
+ help: { type: "boolean", short: "h" },
55
+ },
56
+ allowPositionals: true,
57
+ strict: false,
58
+ tokens: true,
59
+ });
60
+
61
+ for (const token of tokens) {
62
+ if (token.kind === "positional") {
63
+ throw Object.assign(new Error(`Unexpected argument: ${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
38
64
  }
39
- if (arg === "--repo") {
40
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
41
- throw Object.assign(new Error("--repo requires a value (owner/name)"), { code: "INVALID_REPO" });
42
- }
43
- args.repo = argv[++i];
44
- } else if (arg === "--project") {
45
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
46
- throw Object.assign(new Error("--project requires a value (number or node ID)"), { code: "INVALID_PROJECT" });
47
- }
48
- args.project = argv[++i];
49
- } else if (arg === "--older-than") {
50
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
51
- throw Object.assign(new Error("--older-than requires a value (e.g. 30d)"), { code: "INVALID_DURATION" });
52
- }
53
- args.olderThan = argv[++i];
54
- } else if (arg === "--dry-run") {
55
- args.dryRun = true;
56
- } else if (arg === "--help" || arg === "-h") {
57
- args.help = true;
58
- } else {
59
- throw Object.assign(new Error(`Unexpected argument: ${arg}`), { code: "INVALID_ARGS", usage: USAGE });
65
+ if (token.kind !== "option") {
66
+ continue;
67
+ }
68
+ switch (token.name) {
69
+ case "help":
70
+ if (token.value !== undefined) {
71
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
72
+ }
73
+ args.help = true;
74
+ break;
75
+ case "repo":
76
+ args.repo = requireValue(token, "--repo requires a value (owner/name)", "INVALID_REPO");
77
+ break;
78
+ case "project":
79
+ args.project = requireValue(token, "--project requires a value (number or node ID)", "INVALID_PROJECT");
80
+ break;
81
+ case "older-than":
82
+ args.olderThan = requireValue(token, "--older-than requires a value (e.g. 30d)", "INVALID_DURATION");
83
+ break;
84
+ case "dry-run":
85
+ if (token.value !== undefined) {
86
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
87
+ }
88
+ args.dryRun = true;
89
+ break;
90
+ default:
91
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
60
92
  }
61
93
  }
62
94
  return args;
@@ -121,6 +153,37 @@ function parseDuration(raw) {
121
153
  return n * UNIT_MS[m[2]];
122
154
  }
123
155
 
156
+ // ── Settings fallback ──────────────────────────────────────────────────────
157
+
158
+ // Read .devloops (and extension variants) queue settings, mirroring the
159
+ // resolution used by ensure-queue-board.mjs. Returns { project }, { title },
160
+ // and/or { olderThanDays } when configured; never throws on a missing/bad file.
161
+ function resolveSettings(cwd) {
162
+ const basePath = path.join(cwd, ".devloops");
163
+ const extensions = ["", ".yaml", ".yml", ".json"];
164
+ for (const ext of extensions) {
165
+ try {
166
+ const raw = readFileSync(basePath + ext, "utf-8");
167
+ const settings = ext === ".json" ? JSON.parse(raw) : parseYaml(raw);
168
+ const queue = settings?.queue;
169
+ if (!queue) return null;
170
+ const out = {};
171
+ if (typeof queue.projectNumber === "number" && Number.isInteger(queue.projectNumber) && queue.projectNumber > 0) {
172
+ out.project = queue.projectNumber;
173
+ } else if (typeof queue.boardTitle === "string" && queue.boardTitle.trim().length > 0) {
174
+ out.title = queue.boardTitle.trim();
175
+ }
176
+ if (typeof queue.archiveOlderThanDays === "number" && Number.isInteger(queue.archiveOlderThanDays) && queue.archiveOlderThanDays > 0) {
177
+ out.olderThanDays = queue.archiveOlderThanDays;
178
+ }
179
+ return out;
180
+ } catch {
181
+ // extension not present or unparseable — try next
182
+ }
183
+ }
184
+ return null;
185
+ }
186
+
124
187
  // ── API helpers ──────────────────────────────────────────────────────────
125
188
 
126
189
  async function ghGraphql(query, vars, env, runChild = _runChild) {
@@ -289,6 +352,9 @@ function normalizeItem(node) {
289
352
  function selectArchivable(items, { now, olderThanMs }) {
290
353
  return items.filter((it) => {
291
354
  if (it.isArchived) return false;
355
+ // Only archive items in the Done column — a closed issue/PR parked in
356
+ // another column (Backlog/Next Up/In Progress) must be left untouched.
357
+ if (it.status !== "Done") return false;
292
358
  const c = it.content;
293
359
  if (!c || !c.closed || !c.closedAt) return false;
294
360
  const closedAtMs = Date.parse(c.closedAt);
@@ -312,19 +378,36 @@ async function main(args, { env = process.env, runChild } = {}) {
312
378
  const child = runChild ?? _runChild;
313
379
  const repo = validateRepo(args.repo);
314
380
  const [owner] = repo.split("/");
315
- const projectRef = parseProjectRef(args.project);
316
- const olderThanRaw = args.olderThan ?? "30d";
381
+ // Board: explicit --project ref wins; otherwise resolve by board title from
382
+ // .devloops (passed in as args.projectTitle by runCli). Fail closed if neither.
383
+ const hasProjectRef = typeof args.project === "string" && args.project.trim().length > 0;
384
+ const projectRef = hasProjectRef ? parseProjectRef(args.project) : null;
385
+ const projectTitle = !hasProjectRef && typeof args.projectTitle === "string" && args.projectTitle.trim().length > 0
386
+ ? args.projectTitle.trim()
387
+ : null;
388
+ if (!projectRef && !projectTitle) {
389
+ throw Object.assign(
390
+ new Error("--project is required (or configure queue.projectNumber / queue.boardTitle in .devloops)"),
391
+ { code: "INVALID_PROJECT" },
392
+ );
393
+ }
394
+ const olderThanRaw = args.olderThan ?? args.olderThanDefault ?? "7d";
317
395
  const olderThanMs = parseDuration(olderThanRaw);
318
396
  const now = args.now ?? Date.now();
319
397
 
320
398
  const { kind: ownerKind } = await resolveOwner(owner, env, child);
321
399
  const projects = await listAllProjects(owner, ownerKind, env, child);
322
- const project = projectRef.kind === "id"
323
- ? projects.find((p) => p.id === projectRef.value)
324
- : projects.find((p) => p.number === projectRef.value);
400
+ const project = projectRef
401
+ ? (projectRef.kind === "id"
402
+ ? projects.find((p) => p.id === projectRef.value)
403
+ : projects.find((p) => p.number === projectRef.value))
404
+ : projects.find((p) => p.title === projectTitle);
325
405
  if (!project) {
406
+ const desc = projectRef
407
+ ? (projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`)
408
+ : `title "${projectTitle}"`;
326
409
  throw Object.assign(
327
- new Error(`Project ${projectRef.kind === "id" ? `"${projectRef.value}"` : `number ${projectRef.value}`} not found under owner "${owner}"`),
410
+ new Error(`Project ${desc} not found under owner "${owner}"`),
328
411
  { code: "PROJECT_NOT_FOUND" },
329
412
  );
330
413
  }
@@ -378,10 +461,10 @@ async function main(args, { env = process.env, runChild } = {}) {
378
461
 
379
462
  // ── CLI entrypoint ──────────────────────────────────────────────────────
380
463
 
381
- async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
464
+ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
382
465
  let args;
383
466
  try {
384
- args = parseArgs(argv);
467
+ args = parseCliArgs(argv);
385
468
  } catch (err) {
386
469
  stderr.write(`${formatCliError(err)}\n`);
387
470
  process.exitCode = 1;
@@ -391,6 +474,19 @@ async function runCli(argv, { stdout = process.stdout, stderr = process.stderr,
391
474
  stdout.write(USAGE);
392
475
  return;
393
476
  }
477
+
478
+ // Resolve board + threshold defaults from .devloops when the flags are absent.
479
+ // Precedence: explicit --project flag > queue.projectNumber/boardTitle.
480
+ // explicit --older-than flag > queue.archiveOlderThanDays > 7d.
481
+ const settings = resolveSettings(cwd);
482
+ if (args.project === undefined && settings) {
483
+ if (settings.project) args.project = String(settings.project);
484
+ else if (settings.title) args.projectTitle = settings.title;
485
+ }
486
+ if (args.olderThan === undefined && settings?.olderThanDays) {
487
+ args.olderThanDefault = `${settings.olderThanDays}d`;
488
+ }
489
+
394
490
  try {
395
491
  const result = await main(args, { env });
396
492
  stdout.write(JSON.stringify(result) + "\n");
@@ -407,4 +503,4 @@ if (isDirectCliRun(import.meta.url)) {
407
503
  });
408
504
  }
409
505
 
410
- export { main, parseDuration, selectArchivable };
506
+ export { main, parseCliArgs, parseDuration, selectArchivable, resolveSettings, runCli };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { parseArgs } from "node:util";
4
5
  import { parse as parseYaml } from "yaml";
5
6
  import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
6
7
  import { runChild as _runChild } from "../_cli-primitives.mjs";
@@ -32,76 +33,76 @@ Exit codes:
32
33
  3 — board schema/config mismatch (manual reconciliation needed)
33
34
  `;
34
35
 
35
- const VALID_ARGS = new Set(["--repo", "--project", "--title", "--link-repo", "--repair-rename", "--help", "-h"]);
36
+ function parseCliArgs(argv) {
37
+ const parseError = (message) => Object.assign(new Error(message), { usage: USAGE });
38
+ const requireValue = (token, message) => {
39
+ const v = token.value;
40
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
41
+ throw parseError(message);
42
+ }
43
+ return v;
44
+ };
36
45
 
37
- function parseArgs(argv) {
38
- const args = {}; // title default applied in runCli after settings fallback
39
- const consumed = new Set();
40
- for (let i = 0; i < argv.length; i++) {
41
- if (consumed.has(i)) continue;
42
- const arg = argv[i];
43
- if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
44
- throw Object.assign(
45
- new Error(`Unknown flag: ${arg}`),
46
- { code: "INVALID_REPO", usage: USAGE },
47
- );
46
+ const args = {};
47
+ const { tokens } = parseArgs({
48
+ args: [...argv],
49
+ options: {
50
+ repo: { type: "string" },
51
+ project: { type: "string" },
52
+ title: { type: "string" },
53
+ "link-repo": { type: "string" },
54
+ "repair-rename": { type: "boolean" },
55
+ help: { type: "boolean", short: "h" },
56
+ },
57
+ allowPositionals: true,
58
+ strict: false,
59
+ tokens: true,
60
+ });
61
+
62
+ for (const token of tokens) {
63
+ if (token.kind === "positional") {
64
+ throw parseError(`Unexpected argument: ${token.value}`);
48
65
  }
49
- if (arg === "--repo") {
50
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
51
- throw Object.assign(
52
- new Error("--repo requires a value (owner/name)"),
53
- { code: "INVALID_REPO", usage: USAGE },
54
- );
55
- }
56
- args.repo = argv[++i];
57
- consumed.add(i);
58
- } else if (arg === "--project") {
59
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
60
- throw Object.assign(
61
- new Error("--project requires a numeric value"),
62
- { code: "INVALID_PROJECT", usage: USAGE },
63
- );
64
- }
65
- const num = Number(argv[++i]);
66
- if (!Number.isInteger(num) || num <= 0) {
67
- throw Object.assign(
68
- new Error(`--project must be a positive integer, got "${argv[i]}"`),
69
- { code: "INVALID_PROJECT", usage: USAGE },
70
- );
71
- }
72
- args.project = num;
73
- } else if (arg === "--title") {
74
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
75
- throw Object.assign(
76
- new Error("--title requires a value"),
77
- { code: "INVALID_REPO", usage: USAGE },
78
- );
79
- }
80
- args.title = argv[++i];
81
- consumed.add(i);
82
- } else if (arg === "--link-repo") {
83
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
84
- throw Object.assign(
85
- new Error("--link-repo requires a value (owner/name)"),
86
- { code: "INVALID_REPO", usage: USAGE },
87
- );
66
+ if (token.kind !== "option") {
67
+ continue;
68
+ }
69
+ switch (token.name) {
70
+ case "help":
71
+ if (token.value !== undefined) {
72
+ throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
73
+ }
74
+ args.help = true;
75
+ break;
76
+ case "repo":
77
+ args.repo = requireValue(token, "--repo requires a value (owner/name)");
78
+ break;
79
+ case "project": {
80
+ const raw = requireValue(token, "--project requires a numeric value");
81
+ const num = Number(raw);
82
+ if (!Number.isInteger(num) || num <= 0) {
83
+ throw parseError(`--project must be a positive integer, got "${raw}"`);
84
+ }
85
+ args.project = num;
86
+ break;
88
87
  }
89
- args.linkRepo = argv[++i];
90
- consumed.add(i);
91
- } else if (arg === "--repair-rename") {
92
- args.repairRename = true;
93
- } else if (arg === "--help" || arg === "-h") {
94
- args.help = true;
95
- } else {
96
- throw Object.assign(
97
- new Error(`Unexpected argument: ${arg}`),
98
- { code: "INVALID_REPO", usage: USAGE },
99
- );
88
+ case "title":
89
+ args.title = requireValue(token, "--title requires a value");
90
+ break;
91
+ case "link-repo":
92
+ args.linkRepo = requireValue(token, "--link-repo requires a value (owner/name)");
93
+ break;
94
+ case "repair-rename":
95
+ if (token.value !== undefined) {
96
+ throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
97
+ }
98
+ args.repairRename = true;
99
+ break;
100
+ default:
101
+ throw parseError(`Unknown flag: ${token.rawName}`);
100
102
  }
101
103
  }
102
104
  return args;
103
105
  }
104
-
105
106
  // ── Validation ───────────────────────────────────────────────────────────
106
107
 
107
108
  // GitHub slug rules: owner 1-39 chars (alnum/dash, no leading/trailing dash,
@@ -798,7 +799,7 @@ async function main(args, { env = process.env, runChild } = {}) {
798
799
  async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
799
800
  let args;
800
801
  try {
801
- args = parseArgs(argv);
802
+ args = parseCliArgs(argv);
802
803
  } catch (err) {
803
804
  stderr.write(`${formatCliError(err)}\n`);
804
805
  process.exitCode = 1;
@@ -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 list --repo <owner/name> --project <number|id> [--column <name>] [--limit <n>]
6
7
 
@@ -24,69 +25,69 @@ Exit codes:
24
25
  3 — project, field, or column not found
25
26
  `.trim();
26
27
 
27
- const VALID_ARGS = new Set(["--repo", "--project", "--column", "--limit", "--help", "-h"]);
28
+ function parseCliArgs(argv) {
29
+ const parseError = (message) => Object.assign(new Error(message), { usage: USAGE });
30
+ const requireValue = (token, message) => {
31
+ const v = token.value;
32
+ if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
33
+ throw parseError(message);
34
+ }
35
+ return v;
36
+ };
28
37
 
29
- function parseArgs(argv) {
30
38
  const args = {};
31
- for (let i = 0; i < argv.length; i++) {
32
- const arg = argv[i];
33
- if (!VALID_ARGS.has(arg) && arg.startsWith("-")) {
34
- throw Object.assign(
35
- new Error(`Unknown flag: ${arg}`),
36
- { code: "INVALID_ARGS", usage: USAGE },
37
- );
39
+ const { tokens } = parseArgs({
40
+ args: [...argv],
41
+ options: {
42
+ repo: { type: "string" },
43
+ project: { type: "string" },
44
+ column: { type: "string" },
45
+ limit: { type: "string" },
46
+ help: { type: "boolean", short: "h" },
47
+ },
48
+ allowPositionals: true,
49
+ strict: false,
50
+ tokens: true,
51
+ });
52
+
53
+ for (const token of tokens) {
54
+ if (token.kind === "positional") {
55
+ throw parseError(`Unexpected argument: ${token.value}`);
38
56
  }
39
- if (arg === "--repo") {
40
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
41
- throw Object.assign(
42
- new Error("--repo requires a value (owner/name)"),
43
- { code: "INVALID_ARGS", usage: USAGE },
44
- );
45
- }
46
- args.repo = argv[++i];
47
- } else if (arg === "--project") {
48
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
49
- throw Object.assign(
50
- new Error("--project requires a value (number or node ID)"),
51
- { code: "INVALID_ARGS", usage: USAGE },
52
- );
53
- }
54
- args.project = argv[++i];
55
- } else if (arg === "--column") {
56
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
57
- throw Object.assign(
58
- new Error("--column requires a value"),
59
- { code: "INVALID_ARGS", usage: USAGE },
60
- );
61
- }
62
- args.column = argv[++i];
63
- } else if (arg === "--limit") {
64
- if (i + 1 >= argv.length || argv[i + 1].startsWith("-")) {
65
- throw Object.assign(
66
- new Error("--limit requires a positive integer"),
67
- { code: "INVALID_ARGS", usage: USAGE },
68
- );
69
- }
70
- const val = Number(argv[++i]);
71
- if (!Number.isInteger(val) || val < 1) {
72
- throw Object.assign(
73
- new Error(`--limit must be a positive integer, got "${argv[i]}"`),
74
- { code: "INVALID_ARGS", usage: USAGE },
75
- );
57
+ if (token.kind !== "option") {
58
+ continue;
59
+ }
60
+ switch (token.name) {
61
+ case "help":
62
+ if (token.value !== undefined) {
63
+ throw parseError(`Unknown flag: ${token.rawName}=${token.value}`);
64
+ }
65
+ args.help = true;
66
+ break;
67
+ case "repo":
68
+ args.repo = requireValue(token, "--repo requires a value (owner/name)");
69
+ break;
70
+ case "project":
71
+ args.project = requireValue(token, "--project requires a value (number or node ID)");
72
+ break;
73
+ case "column":
74
+ args.column = requireValue(token, "--column requires a value");
75
+ break;
76
+ case "limit": {
77
+ const raw = requireValue(token, "--limit requires a positive integer");
78
+ const val = Number(raw);
79
+ if (!Number.isInteger(val) || val < 1) {
80
+ throw parseError(`--limit must be a positive integer, got "${raw}"`);
81
+ }
82
+ args.limit = val;
83
+ break;
76
84
  }
77
- args.limit = val;
78
- } else if (arg === "--help" || arg === "-h") {
79
- args.help = true;
80
- } else {
81
- throw Object.assign(
82
- new Error(`Unexpected argument: ${arg}`),
83
- { code: "INVALID_ARGS", usage: USAGE },
84
- );
85
+ default:
86
+ throw parseError(`Unknown flag: ${token.rawName}`);
85
87
  }
86
88
  }
87
89
  return args;
88
90
  }
89
-
90
91
  // ── Validation ───────────────────────────────────────────────────────────
91
92
 
92
93
  const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
@@ -460,7 +461,7 @@ async function main(args, { env = process.env, runChild } = {}) {
460
461
  async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env } = {}) {
461
462
  let args;
462
463
  try {
463
- args = parseArgs(argv);
464
+ args = parseCliArgs(argv);
464
465
  } catch (err) {
465
466
  stderr.write(`${formatCliError(err)}\n`);
466
467
  process.exitCode = 1;