Skip to content

refactor(cli): use Node argument parsing - #231

Merged
kyle-sexton merged 1 commit into
mainfrom
agent/issue-218-parse-args
Jul 21, 2026
Merged

refactor(cli): use Node argument parsing#231
kyle-sexton merged 1 commit into
mainfrom
agent/issue-218-parse-args

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

  • replace the four hand-written CLI parsers with strict node:util.parseArgs configurations
  • preserve existing option mappings, defaults, package environment fallback, output prefixes, and exit codes
  • add focused parser-contract tests across concurrency, Dependabot, runner, and package-lifecycle entry points

Behavior

The standard parser intentionally adds normal Node CLI syntax and strictness:

  • -- is an option terminator; policy CLIs accept it with no trailing positional, while package paths after it remain positional
  • string options accept --name=value
  • dash-leading string values must use the inline form (for example, --root=-repo); the separated form is rejected as ambiguous
  • package-lifecycle unknown options are rejected unless placed after --

Repeated options remain last-wins. The policy CLIs still wrap argument failures as ConfigurationError and exit 2 with their established prefixes; package-lifecycle still exits 1 and preserves PACKAGE_BASE_REF fallback.

Research

The implementation and tests follow the stable Node.js util.parseArgs contract, including strict unknown-option/type validation, string and boolean option definitions, defaults, positionals, inline values, and the option terminator.

Verification

Passed on Node 24.18.0:

  • affected suites: concurrency 24/24, Dependabot 35/35, runner 228/228, package lifecycle 14/14
  • changed-file Biome and pre-commit hooks (Biome, EditorConfig, typos, gitleaks)
  • Markdown lint, Lefthook configuration validation, all three policy audits
  • Lefthook .NET tests 12/12 and shell harness
  • direct CLI error-contract checks: policy entry points exit 2; package-lifecycle exits 1
  • cross-document reconciliation found no prose that promises the superseded parsing behavior and no residual hand-written parser loops

distribution/sync-manifest.sh validate was attempted against the staged change but exceeded the 60-second local Windows cap without producing a finding; CI remains the authoritative run for that gate. The managed concurrency, Dependabot, and runner source files will propagate through the existing manifest; their tests and the package harness remain repository-local.

Related

Closes #218

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@claude

claude Bot commented Jul 21, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 40s —— View job


PR Review

  • Gather context and read project guidelines
  • Read REVIEW.md criteria
  • Review the PR diff
  • Analyze changed implementation files
  • Analyze changed test files
  • Post review findings

Summary

The refactor is well-structured: the four hand-written parsers are replaced consistently, the environment injection pattern is extended to runner-policy (good for testability), and the new tests cover the contract precisely — defaults, inline = form, dash-leading inline values, unknown-option rejection, ambiguous-value rejection, positional rejection, and the -- terminator. No secrets, injection surfaces, or multi-tenant concerns are in scope.

One significant concern and two nits follow.


🔴 Important — Unrelated policy entry removed from runner-policy/policy.json

components/runner-policy/policy.json is modified in this commit but not listed among the PR's eight changed files in the description, and the PR body makes no mention of it. The diff removes the claude-security-review.yml@99cb082e runner-policy entry (routing, allowedInputs, allowedSecrets, allowedCallerPermissions) in full:

-    "melodic-software/ci-workflows/.github/workflows/claude-security-review.yml@99cb082ebb942b9da08e8345a851be5bf252ad79": {
-      "routing": "runner-input",
-      "runnerInput": "runner",
-      "allowedInputs": ["runner", "skip-actors", "paths"],
-      "allowedSecrets": {
-        "CLAUDE_CODE_OAUTH_TOKEN": "${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}"
-      },
-      "allowedCallerPermissions": { ... }
-    },

This is a policy-enforcement change bundled silently into a CLI-parsing refactor. Removing an entry from runner-policy/policy.json changes which caller workflows are authorised to use the fleet runner — that has a direct security and operational impact (callers using this SHA-pinned workflow become unauthorised). If this removal is intentional it needs to be called out in the PR body with a justification (workflow retired, SHA rotated, etc.); if it's accidental it should be reverted before merge.

Fix this →


🟡 Nit — cause is dropped when wrapping parseArgs errors

All three policy-CLI parsers catch parseArgs errors and re-throw as ConfigurationError, but only the message is forwarded:

// e.g. components/runner-policy/runner-policy.mjs
} catch (error) {
  throw new ConfigurationError(error instanceof Error ? error.message : String(error));
}

The original TypeError from parseArgs — including its stack — is lost. If ConfigurationError accepts an options object (like Error does in ES2022), passing { cause: error } would preserve the chain for downstream tooling and debugging. If ConfigurationError doesn't support cause yet, this is a pre-existing gap rather than a regression introduced here.

Permalink: runner-policy.mjs line ~329 (same pattern in the other two parsers).


🟡 Nit — --repository-visibility has no default: while every other string option does

In runner-policy.parseArguments, all other type: "string" options declare a default:

config: { default: DEFAULT_CONFIG_PATH, type: "string" },
policy: { default: DEFAULT_POLICY_PATH, type: "string" },
root:   { default: process.cwd(),       type: "string" },
"repository-visibility": { type: "string" },   // no default

The intent (fall back to environment.CI_REPOSITORY_VISIBILITY via ??) is correct and tested, but the asymmetry can mislead a reader into thinking the default was forgotten. A short comment or a // fallback: environment.CI_REPOSITORY_VISIBILITY annotation on that line would eliminate the question.

Permalink: runner-policy.mjs line ~314


Everything else looks solid: the strict: true + allowPositionals: false pair correctly rejects positionals and unknown flags for the policy CLIs; the -- terminator tests confirm the new semantics; the environment injection in runner-policy is a clean testability improvement that mirrors the existing package-lifecycle pattern; and the ?? fallback for repository-visibility correctly gives CLI flag priority over env var.

@kyle-sexton
kyle-sexton merged commit b53612e into main Jul 21, 2026
42 checks passed
@kyle-sexton
kyle-sexton deleted the agent/issue-218-parse-args branch July 21, 2026 19:48
kyle-sexton added a commit that referenced this pull request Jul 29, 2026
…true form (#294)

## Summary

`components/claude-lanes/claude-review.yml` justified its job-level
queue block
with a key-level claim:

> `queue:` cannot share a concurrency block with cancel-in-progress

GitHub's prohibition is **value-level**, not key-level. Verbatim, from
the
reusable that renders into both `#concurrency` and
`#jobsjob_idconcurrency`:

> The combination of `queue: max` and `cancel-in-progress: true` is not
allowed
> and will result in a workflow validation error.

Same file, restated in the example prose:

> Note that `queue: max` cannot be combined with `cancel-in-progress:
true`,
> because the two options describe conflicting behaviors for handling
> in-progress runs.

- Source pinned at the commit read: [`github/docs@336b7f5`

`data/reusables/actions/actions-group-concurrency.md`](https://github.com/github/docs/blob/336b7f546d9443dab4e1fa4f0f470e45448c7abc/data/reusables/actions/actions-group-concurrency.md)
  (lines 20 and 126).
- Rendered page carrying the same sentence, confirmed by fetch:

<https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency>

`queue: max` + `cancel-in-progress: false` is legal and runs — the
key-level
wording forbids a shape GitHub permits.

The correction narrows the claim to the documented pair and adds the
second,
independent reason the two blocks stay separate: this group is
job-scoped and
repo-wide, not per-PR. Without that clause a reader who sets
`cancel-in-progress: false` could conclude the blocks may now be merged,
which
would silently collapse the per-PR supersede group into the repo-wide
queue.

The authoritative URL is cited **inline, in the tracked bytes**
(`565559a`,
added after review). Because this component is materialized verbatim
downstream, a citation living only in a commit message never reaches the
maintainers who read the claim — a correct-but-uncited assertion would
repeat
this PR's own failure mode at lower severity. The link sits at the
sentence that
defers to it per
`conventions/engineering/documentation-and-citations.md`, and
it is a living URL rather than a pinned snapshot because the same
convention
prefers fetching at read time over storing a snapshot with no recheck
trigger.
The job-level anchor (`#jobsjob_idconcurrency`) is cited rather than the
workflow-level one because the comment annotates a
`jobs.<id>.concurrency`
block; both anchors were confirmed live to exist on the rendered page
and to
carry the rule sentence.

### Why this races the open sync PRs

This component is sync-managed: its bytes are copied verbatim into every
consumer's `.github/workflows/claude-review.yml`. The wording has not
landed
anywhere yet, and four open sync PRs are carrying it right now.

Regenerated from commands, not recalled:

```console
$ grep -n '^targets:' distribution/sync-manifest.yml
216:targets:
$ awk 'NR>=216' distribution/sync-manifest.yml | grep -c '^      - claude-review-caller$'
5
```

Managed targets: `claude-code-plugins`, `dotfiles`, `github-iac`,
`medley`,
`provisioning`.

Each consumer's live default-branch file was fetched and counted — the
file was
read, not grepped on a ref for text expected to be there:

| Repo | `queue: max` on default branch | old wording on default branch
| open sync PR head carries old wording |
| --- | --- | --- | --- |
| `claude-code-plugins` | 0 | 0 | no open sync PR |
| `dotfiles` | 0 | 0 | #361 (`df035f9`) — yes |
| `github-iac` | 0 | 0 | #244 (`0b8124d`) — yes |
| `medley` | 0 | 0 | #1676 (`d107862`) — yes |
| `provisioning` | 0 | 0 | #231 (`7b9d016`) — yes |

Landing this before those PRs merge means the correct text reaches every
consumer on first contact. Landing it after means a false statement
propagates
fleet-wide and needs a second sync to retract.

**Hold on the four sync PRs until this merges.** The rollout-window gate
is
intact — `grep -c '^ automerge: false$' distribution/sync-manifest.yml`
returns `8` against `8` total targets — and each of the four PRs was
queried
live (`gh pr view --json autoMergeRequest`): **none is armed**. So
nothing
merges them without a human, and a human merging any of them before #294
lands
is the only thing that defeats this PR.

Merging #294 does **not** require closing them. Verified in the engine
at the
SHA this repository pins (`ci-workflows@ac223bb`,
`.github/workflows/standards-sync.yml:454-461`): it uses
`peter-evans/create-pull-request` against a fixed `branch:
chore/standards-sync`,
which is the head branch on all four PRs — so a subsequent real run
refreshes
each existing PR in place rather than opening a new one, which is also
why the
engine guards auto-merge arming on `pull-request-operation ==
'created'`.
`sync.yml` runs on `push: branches: [main]`, so merging this PR is
itself the
refresh trigger.

### Sibling component: checked, no change

`components/claude-lanes/claude-security-review.yml` was read in full,
not
assumed to match. It makes no key-level claim. Its one queue-adjacent
statement — "a full queue CANCELS new arrivals" — is accurate:

> `max`: Up to 100 jobs or workflow runs can be `pending` in the
concurrency
> group. When the queue is full, any additional jobs or workflow runs
are
> canceled.

A repo-wide grep confirms the defect had exactly one site:

```console
$ grep -rn 'share a concurrency block\|cannot share' . | grep -v '^\./\.git/'
./components/claude-lanes/claude-review.yml:87: ...
```

This repository's own `.github/workflows/claude-review.yml` sets no
caller-level
concurrency and no `queue:`, so it never carried the claim.

### Deliberate non-change

Independent verification surfaced a separate omission, not a falsehood:
the
comment does not mention that `queue: max` caps at 100 pending and
cancels
arrivals beyond that. Left out on purpose — this PR narrows a false
claim and
should stay a one-hunk diff while it races the sync PRs.

The overflow-wedge argument that makes the cap load-bearing in the
security
lane does not transfer here, and that was checked rather than assumed:
every
ruleset on all five managed targets was enumerated and its
`required_status_checks` contexts read, and **no target requires any
`claude`-named context today**.

```console
$ # per target: enumerate rulesets, union their required contexts, count claude ones
claude-code-plugins: total_required=4 claude_required=0
dotfiles:            total_required=3 claude_required=0
github-iac:          total_required=3 claude_required=0
medley:              total_required=3 claude_required=0
provisioning:        total_required=3 claude_required=0
```

So overflow cancellation on this lane cannot void a required check — the
same
premise that already makes this lane's deliberate `cancel-in-progress:
true`
safe. If a consumer later promotes the code-review context to required,
the
cap becomes load-bearing and the comment should gain it. Worth a
follow-up on
its own merits, not a blocker for this one.

## Test plan

- `components/claude-lanes/claude-lanes.test.sh`, counted from the run
rather
  than eyeballed, and reproduced by two independent runs:

  ```console
$ bash components/claude-lanes/claude-lanes.test.sh > run.txt 2>&1; echo
"exit=$?"
  exit=0
  $ grep -cE '^PASS' run.txt; grep -cE '^FAIL' run.txt
  30
  0
  ```

  That includes `[29] a synced lane caller fails actionlint without the
suppression` and `[30] control run reports the suppressed message`. Note
for
anyone re-running: the harness materializes from the git **index**, so
the
change must be staged or every target reports `source worktree bytes
differ
  from the indexed object` and the suite fails for that reason alone.
- Comment-only change: no YAML key, value, group expression, or pin is
touched.
Confirmed by `git diff --stat origin/main...HEAD` — 6 insertions, 3
deletions,
  every line inside a `#` comment block.
- Independent verification by a fresh-context agent with the rationale
withheld
(given the final file text and asked whether every claim is true, and
whether
the constraint is stated at the correct level of generality). Verdict on
the
corrected wording: **ACCURATE** — "The comment restates the constraint
at
precisely the docs' generality — the docs name the literal `queue: max`
+
  `cancel-in-progress: true` pair and nothing wider. Neither broader nor
narrower. I would not reword this clause." It also independently
confirmed
  `queue:` is valid at job level, via
`data/reusables/actions/jobs/section-using-concurrency-jobs.md`
including the
  same reusable.
- Repository CI on this PR: every check in the `pass` bucket, zero
non-pass,
  verified by `gh pr checks 294 --json name,bucket` on each pushed head
  (`a2a8228` and `565559a`). That includes the Claude review lane
  (`review / review`) — run once per head, never re-run to chase green.

## Related

- #286 — the PR that introduced these caller components and the wording
  corrected here.
- Open sync PRs carrying the pre-correction text:
melodic-software/dotfiles#361,
  melodic-software/provisioning#231, melodic-software/github-iac#244,
  melodic-software/medley#1676.
- Two sibling sites for the same claim live in `ci-workflows` and are
tracked
there (ci-workflows#296 for the workflow; ci-workflows#302, merged, for
  `PLAN.md`). Deliberately untouched by this PR.

No linked issue.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reinvented-wheel: hand-rolled CLI arg parsing duplicates node:util.parseArgs

1 participant