Skip to content

Fix agent-help hidden-flag leak; make check-surface a real drift gate#542

Open
jeremy wants to merge 3 commits into
mainfrom
stale-surface
Open

Fix agent-help hidden-flag leak; make check-surface a real drift gate#542
jeremy wants to merge 3 commits into
mainfrom
stale-surface

Conversation

@jeremy

@jeremy jeremy commented Jul 18, 2026

Copy link
Copy Markdown
Member

Closes #539.

TL;DR

#539 asked to reconcile the committed .surface snapshot with scripts/check-cli-surface.sh, on the premise that the file had drifted from its generator and CI didn't catch it. Investigating, the premise doesn't hold: .surface is owned and enforced by a different generator, which it already matches exactly. So instead of regenerating the file, this PR fixes the two things that are actually wrong and turns the misleading check-surface target into a real gate.

What's actually going on

There are two independent surface generators, and they disagree:

scripts/check-cli-surface.sh (shell) surface.SnapshotString (Go, github.com/basecamp/cli)
Walks --help --agent JSON the Cobra command tree
Globals per subcommand curated → salient only (post-#499) full inherited globals
Hidden flags (--assignee) emitted excluded
--version leak (#368) had it pre-#499 never had it (--version is local, not inherited)
Output ~7.6k lines ~16.6k lines
Enforced by nothing — check-surface just echoed a /tmp line count TestSurfaceSnapshot (internal/commands/surface_test.go)

The committed .surface is byte-identical (modulo trailing newline) to the Go generator's output. TestSurfaceSnapshot passes precisely because .surface == surface.SnapshotString(root), and that test is the CI catch #539 thought was missing. Cross-version compatibility is separately handled by check-surface-compatcheck-cli-surface-diff.sh, entirely on the script side.

Regenerating .surface to the shell script's curated shape (the literal ask) would have:

  • permanently reddened TestSurfaceSnapshot (the two generators can't both own the file),
  • churned ~9k lines,
  • forced ~9 false-drift baselines into .surface-skill-drift for globals (--jq, --agent, --profile) that do work on those subcommands — curation drops true information, and
  • enshrined an agent-help bug (below) as canonical surface.

What this PR changes instead (4 files, zero .surface churn)

1. Fix the agent-help hidden-flag leak (internal/cli/root.go)
The "2 additions" #539 noticed (FLAG basecamp recordings [list] --assignee) are a leak, not new surface. --assignee is MarkHidden with usage "Not supported — use reports assigned instead". emitAgentHelp surfaced it anyway because pflag's VisitAll visits hidden flags. Skip hidden flags at all three emission sites so agent-help matches text --help and the tree walker. Legitimate visible --assignee/--assignees flags on cards/todos are unaffected. Regression test in help_test.go.

2. Make check-surface a real gate (Makefile)
It regenerated to /tmp and echoed a count — caught nothing but a totally broken binary. Repoint it at TestSurfaceSnapshot (the authority) so it fails on drift and points at a new make update-surface regeneration target. check-surface-compat / the cli-surface CI job are untouched (they diff generated-vs-generated across versions and never read the committed file).

3. Idempotent regeneration (internal/commands/surface_test.go)
-update-surface wrote .surface without a trailing newline (stripping the POSIX one, creating churn). Add one so make update-surface is idempotent.

Verification

  • make check-surface passes, and bites when .surface drifts (pointing at make update-surface).
  • make update-surface is idempotent (16,586 lines, no diff).
  • basecamp recordings --help --agent no longer emits --assignee; the shell script's output drops the 2 leak lines while keeping the 8 legit visible ones.
  • TestSurfaceSnapshot, check-skill-drift, check-smoke-coverage green; bin/ci exits 0.

Follow-up note for #539

The shell script and .surface still legitimately differ on the curated-vs-full-globals axis, but no consumer cross-checks them, so it's benign. If we later want a single source of truth, the options are: (a) keep the Go generator as authority (status quo after this PR), (b) migrate the snapshot + compat gate onto the shell script and retire TestSurfaceSnapshot/.surface-breaking, or (c) reconcile the two generators in github.com/basecamp/cli. This PR takes (a) as the smallest correct step.


Summary by cubic

Fixes the hidden flag leak in agent help and turns check-surface into a real drift gate bound to TestSurfaceSnapshot. Refines update-surface to only rewrite .surface when the snapshot actually changes while still handling removal-only updates, addressing #539 without churn.

  • Bug Fixes

    • Agent help no longer emits hidden flags (e.g., recordings list --assignee), matching text --help and the command tree.
    • Added TestAgentHelpOmitsHiddenFlags to prevent regressions.
  • Refactors

    • check-surface now runs TestSurfaceSnapshot and fails on drift with guidance to run make update-surface; make help updated.
    • update-surface writes a trailing newline, updates .surface on acknowledged removal-only changes, and only rewrites when the snapshot differs via shouldWriteBaseline; added TestShouldWriteBaseline.

Written for commit c209032. Summary will update on new commits.

Review in cubic

The committed .surface snapshot is generated and enforced by the Go
surface package (surface.SnapshotString) via TestSurfaceSnapshot, not by
scripts/check-cli-surface.sh. The two generators diverge: the Go one
walks the command tree (full inherited globals, excludes hidden flags,
no --version leak), while the shell script walks --help --agent (curated
salient globals post-#499, and — the bug here — emits hidden flags).

Rather than churn .surface to the script's shape (which would break
TestSurfaceSnapshot and enshrine the leak), fix the underlying issues:

- emitAgentHelp leaked hidden flags because pflag's VisitAll visits them,
  so `basecamp recordings --help --agent` surfaced the MarkHidden'd,
  "Not supported" --assignee flag. Skip hidden flags at all three
  emission sites to match text --help and the tree walker. Add a
  regression test.

- check-surface previously regenerated to /tmp and echoed a line count,
  catching nothing. Repoint it at TestSurfaceSnapshot (the authority) so
  it actually fails on drift, and add an update-surface target for
  regeneration. check-surface-compat / the cli-surface CI job are
  unchanged (they diff generated-vs-generated across versions).

- TestSurfaceSnapshot's -update-surface wrote .surface without a trailing
  newline; add one so update-surface is idempotent and POSIX-clean.

Closes #539.
Copilot AI review requested due to automatic review settings July 18, 2026 06:35
@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) bug Something isn't working labels Jul 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #539 by fixing an agent-help bug (hidden flags leaking into --help --agent JSON output) and by turning make check-surface into a real drift gate tied to the authoritative TestSurfaceSnapshot, while keeping the committed .surface snapshot owned by the Go generator (no churn).

Changes:

  • Filter hidden flags out of agent-help output so --help --agent matches text help behavior.
  • Rework make check-surface to run TestSurfaceSnapshot and add an update-surface target for regeneration guidance.
  • Make .surface regeneration idempotent by ensuring it is written with a trailing newline.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
Makefile Makes check-surface enforce .surface drift via TestSurfaceSnapshot and adds an update-surface target.
internal/commands/surface_test.go Ensures regenerated .surface includes a trailing newline for idempotence.
internal/cli/root.go Filters hidden flags out of agent-help JSON emission across local/parent-scoped/inherited flag sources.
internal/cli/help_test.go Adds a regression test asserting hidden flags (e.g. recordings --assignee) do not appear in agent-help output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Makefile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21a3d68ee9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Makefile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread Makefile
TestSurfaceSnapshot's -update-surface only wrote .surface when there were
additions, so a removal-only change (removed lines acknowledged in
.surface-breaking) left the removed CMD/FLAG lines in .surface — and
consumers that read it as the current surface (check-skill-drift,
check-smoke-coverage) would keep validating removed commands. Write
whenever there are no unacknowledged removals instead.
Copilot AI review requested due to automatic review settings July 18, 2026 06:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment on lines +18 to +20
// Trailing newline so -update-surface writes a POSIX-clean file; the
// comparisons below TrimSpace, so it does not affect drift detection.
current := surface.SnapshotString(root) + "\n"
Comment thread internal/cli/help_test.go
Comment on lines +541 to +547
var buf bytes.Buffer
cmd := NewRootCmd()
cmd.AddCommand(commands.NewRecordingsCmd())
cmd.SetOut(&buf)
cmd.SetArgs([]string{"recordings", "list", "--help", "--agent"})
_ = cmd.Execute()

Refine the -update-surface write condition into a testable predicate,
shouldWriteBaseline: write when update mode is on, there are no
unacknowledged removals, and the baseline bytes differ from the freshly
generated surface. The byte-difference guard keeps removal-only updates
(acknowledged in .surface-breaking) writing while making an unchanged
surface a true no-op. Add a TestShouldWriteBaseline regression test
covering removal-only, addition, no-change, unacknowledged-removal, and
non-update cases.

Also refresh 'make help': describe check-surface as the drift gate and
list the new update-surface target.
Copilot AI review requested due to automatic review settings July 18, 2026 07:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Comment thread Makefile
Comment on lines +362 to 365
@BASECAMP_NO_KEYRING=1 $(GOTEST) $(BUILD_TAGS) ./internal/commands/ -run TestSurfaceSnapshot -count=1 || { \
echo; echo "CLI surface drift: committed .surface is stale. Run: make update-surface"; \
exit 1; \
}
Comment thread internal/cli/help_test.go
Comment on lines +548 to +561
var info struct {
Flags []struct {
Name string `json:"name"`
} `json:"flags"`
}
require.NoError(t, json.Unmarshal(buf.Bytes(), &info))

names := make([]string, len(info.Flags))
for i, f := range info.Flags {
names[i] = f.Name
}
assert.NotContains(t, names, "assignee", "hidden flag leaked into agent help")
assert.Contains(t, names, "limit", "visible flag should still be present")
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c209032cd5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Makefile
echo "ERROR: jq is required for check-surface but was not found."; \
echo "Install with: brew install jq (macOS), apt-get install jq (Debian/Ubuntu)"; \
check-surface: check-toolchain
@BASECAMP_NO_KEYRING=1 $(GOTEST) $(BUILD_TAGS) ./internal/commands/ -run TestSurfaceSnapshot -count=1 || { \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require check-surface to fail on stale acknowledged removals

When a command or flag is removed and the author only adds the old line to .surface-breaking, this target still passes because TestSurfaceSnapshot filters acknowledged removals out before reporting failures; it then prints .surface as up to date even though the removed entries remain in the committed snapshot. The fresh issue after the update-surface fix is the non-update check path here: consumers like check-skill-drift and check-smoke-coverage still read stale .surface, so check-surface should also require the baseline to match the generated snapshot once removals are acknowledged.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working commands CLI command implementations tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reconcile .surface snapshot with the post-#499 generator

2 participants