dev-loops 0.2.5 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.claude/.claude-plugin/plugin.json +9 -2
  2. package/.claude/agents/dev-loop.md +2 -1
  3. package/.claude/hooks/_bash-command-classify.mjs +164 -0
  4. package/.claude/hooks/_hook-decisions.mjs +130 -0
  5. package/.claude/hooks/_hook-io.mjs +1 -1
  6. package/.claude/hooks/_run-context.mjs +179 -0
  7. package/.claude/hooks/post-tool-use-merge.mjs +1 -1
  8. package/.claude/hooks/pre-tool-use-bash-gate.mjs +2 -2
  9. package/.claude/hooks/pre-tool-use-write-guard.mjs +1 -1
  10. package/.claude/skills/dev-loop/SKILL.md +5 -5
  11. package/.claude/skills/docs/merge-preconditions.md +15 -0
  12. package/.claude/skills/docs/pr-lifecycle-contract.md +1 -0
  13. package/CHANGELOG.md +52 -0
  14. package/agents/dev-loop.agent.md +5 -1
  15. package/cli/index.mjs +3 -1
  16. package/package.json +2 -2
  17. package/scripts/_cli-primitives.mjs +2 -0
  18. package/scripts/claude/generate-claude-assets.mjs +71 -3
  19. package/scripts/docs/validate-links.mjs +20 -11
  20. package/scripts/github/capture-review-threads.mjs +32 -14
  21. package/scripts/github/detect-checkpoint-evidence.mjs +28 -11
  22. package/scripts/github/detect-linked-issue-pr.mjs +22 -10
  23. package/scripts/github/manage-sub-issues.mjs +37 -16
  24. package/scripts/github/probe-copilot-review.mjs +24 -12
  25. package/scripts/github/ready-for-review.mjs +22 -10
  26. package/scripts/github/reconcile-draft-gate.mjs +22 -10
  27. package/scripts/github/reply-resolve-review-threads.mjs +34 -15
  28. package/scripts/github/request-copilot-review.mjs +28 -11
  29. package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
  30. package/scripts/github/stage-reviewer-draft.mjs +32 -14
  31. package/scripts/github/upsert-checkpoint-verdict.mjs +49 -26
  32. package/scripts/github/write-gate-findings-log.mjs +41 -20
  33. package/scripts/loop/_loop-pr-aggregation.mjs +45 -0
  34. package/scripts/loop/build-handoff-envelope.mjs +32 -14
  35. package/scripts/loop/conductor-monitor.mjs +26 -47
  36. package/scripts/loop/conductor.mjs +31 -12
  37. package/scripts/loop/copilot-pr-handoff.mjs +31 -14
  38. package/scripts/loop/debt-remediate.mjs +28 -11
  39. package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
  40. package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
  41. package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
  42. package/scripts/loop/detect-internal-only-pr.mjs +31 -13
  43. package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
  44. package/scripts/loop/detect-pr-gate-coordination-state.mjs +28 -11
  45. package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
  46. package/scripts/loop/detect-stale-runner.mjs +32 -14
  47. package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
  48. package/scripts/loop/info.mjs +28 -10
  49. package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
  50. package/scripts/loop/inspect-run.mjs +35 -16
  51. package/scripts/loop/outer-loop.mjs +35 -16
  52. package/scripts/loop/pr-runner-coordination.mjs +31 -12
  53. package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
  54. package/scripts/loop/pre-flight-gate.mjs +25 -9
  55. package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
  56. package/scripts/loop/pre-push-main-guard.mjs +19 -5
  57. package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
  58. package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
  59. package/scripts/loop/run-conductor-cycle.mjs +25 -47
  60. package/scripts/loop/run-refinement-audit.mjs +44 -22
  61. package/scripts/loop/run-watch-cycle.mjs +28 -12
  62. package/scripts/loop/steer-loop.mjs +122 -62
  63. package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
  64. package/scripts/projects/archive-done-items.mjs +410 -0
  65. package/scripts/projects/reorder-queue-item.mjs +334 -76
  66. package/scripts/refine/_refine-helpers.mjs +21 -9
  67. package/scripts/refine/verify.mjs +31 -13
  68. package/skills/dev-loop/SKILL.md +9 -5
  69. package/skills/docs/merge-preconditions.md +15 -0
  70. package/skills/docs/pr-lifecycle-contract.md +1 -0
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { runChild, requireOptionValue } from "../_cli-primitives.mjs";
2
+ import { requireTokenValue } from "../_cli-primitives.mjs";
3
3
  import {
4
4
  buildParseError,
5
5
  formatCliError,
6
6
  isDirectCliRun,
7
- parseJsonText,
8
7
  } from "../_core-helpers.mjs";
9
8
  import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
10
9
  import { detectPrGateCoordinationState } from "./detect-pr-gate-coordination-state.mjs";
@@ -14,9 +13,11 @@ import {
14
13
  SUBAGENT_ACTIONS as SHARED_SUBAGENT_ACTIONS,
15
14
  buildHandoffContractForConductorAction,
16
15
  } from "./_handoff-contract.mjs";
16
+ import { listOpenPrs } from "./_loop-pr-aggregation.mjs";
17
+ import { parseArgs } from "node:util";
18
+ export { listOpenPrs };
17
19
  const USAGE = `Usage: run-conductor-cycle.mjs --repo <owner/name>
18
20
  Poll all open PRs, detect state, and output an ordered action queue.`.trim();
19
- const OPEN_PR_LIST_LIMIT = 1000;
20
21
  export const CHECKPOINT_ACTION_TO_CONDUCTOR_ACTION = Object.freeze({
21
22
  [PR_CHECKPOINT_ACTION.ADDRESS_REVIEW_FEEDBACK]: "fix_threads",
22
23
  [PR_CHECKPOINT_ACTION.REPLY_RESOLVE_REVIEW_THREADS]: "fix_threads",
@@ -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>");
@@ -90,43 +105,6 @@ export function parseCliArgs(argv) {
90
105
  }
91
106
  return options;
92
107
  }
93
- export async function listOpenPrs({ repo }, { env, ghCommand }) {
94
- const result = await runChild(
95
- ghCommand,
96
- [
97
- "pr",
98
- "list",
99
- "--repo",
100
- repo,
101
- "--state",
102
- "open",
103
- "--limit",
104
- String(OPEN_PR_LIST_LIMIT),
105
- "--json",
106
- "number,title,url,isDraft,headRefName,author",
107
- ],
108
- env,
109
- );
110
- if (result.code !== 0) {
111
- const detail = result.stderr.trim() || `exit code ${result.code}`;
112
- throw new Error(`gh command failed: ${detail}`);
113
- }
114
- const payload = parseJsonText(result.stdout);
115
- if (!Array.isArray(payload)) {
116
- throw new Error("Invalid gh pr list payload: expected an array");
117
- }
118
- return payload
119
- .map((pr) => ({
120
- number: Number.isInteger(pr?.number) ? pr.number : null,
121
- title: typeof pr?.title === "string" ? pr.title : "",
122
- url: typeof pr?.url === "string" ? pr.url : null,
123
- isDraft: Boolean(pr?.isDraft),
124
- headRefName: typeof pr?.headRefName === "string" ? pr.headRefName : null,
125
- authorLogin: typeof pr?.author?.login === "string" ? pr.author.login : null,
126
- }))
127
- .filter((pr) => pr.number !== null)
128
- .sort((left, right) => left.number - right.number);
129
- }
130
108
  export async function detectPrState(
131
109
  pr,
132
110
  {
@@ -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>");
@@ -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)) {