Skip to content

feat(cli): wrap config, completion, and feedback - #35

Merged
replygirl merged 6 commits into
mainfrom
worktree-openspec-passthroughs
Sep 8, 2026
Merged

feat(cli): wrap config, completion, and feedback#35
replygirl merged 6 commits into
mainfrom
worktree-openspec-passthroughs

Conversation

@replygirl

Copy link
Copy Markdown
Contributor

Why

openspec-1-11-parity closed every everyday OpenSpec surface except three, and
named them in its own Non-Goals: config, completion, and feedback. Until
they are wrapped, the standing rule — never call bare openspec — is
unenforceable in practice: a user who wants to inspect the machine-global
config, install shell completion, or file a bug has no cospec command to reach
for.

The gap is not only cosmetic. cospec's WRAPPED_ENV forces
OPENSPEC_TELEMETRY=0 and OPENSPEC_NO_COMPLETIONS=1 on every wrapped call,
and cospec generates its harness files from cospec canon rather than from
OpenSpec's profile/workflows/delivery keys — so several global config keys
are genuinely misleading when read through a cospec repo, and nothing told the
user so. defaultStore is a key cospec already reads during root resolution
but offered no way to set.

Each surface got its own decision rather than one blanket treatment, because
each one fails differently.

config — a disciplined passthrough that deliberately skips the shared helper

cospec config <path|list|get|set|unset|reset|edit|profile> wraps
openspec config, and it is the one passthrough command that does not route
through core/passthrough-command.ts. Every one of that helper's three
unconditional appends is wrong here:

  • --store is not a config option at all — upstream declares a parent-level
    --scope, and OpenSpec config is machine-global, so resolveRoot and
    root.storeArgs never apply. cospec config --store <id> is refused with
    exit 1 and a named message rather than absorbed and ignored.
  • --json exists on list alone.
  • A trailing --no-color turned out to be accepted (commander resolves the
    program-level flag from a leaf), so it is merely redundant — cospec's spawn
    already prefixes one, so config never appends a second.

Adding three opt-outs to a helper that serves eight happy callers would make it
harder to reason about, so commands/config.ts gets a local, pure argv builder,
the way commands/workset.ts already does. --scope is hoisted to sit between
config and the subcommand, where upstream expects it.

The surface splits into two call classes:

  • Class A (path, list, get, set, unset, reset --all -y,
    profile <preset>) is a piped passthroughOpenspec call with
    expect.exitCodes = [0, 1], because upstream uses exit 1 for ordinary
    negative results (a missing key) that are results to relay, not wrapped-call
    violations.
  • Class B (edit, bare profile, reset --all without -y) is a terminal
    handover — inherited stdio, shell: false, verbatim child exit code including
    130 on cancellation. Upstream spawns $EDITOR and runs @inquirer menus,
    which cannot survive cospec's piped stdin: 'ignore' spawn. This turns
    workset open's one-off into a named class with a fixed contract, documented
    in docs/architecture.md and specified in openspec-read-passthroughs.

The one-JSON-document invariant holds on every subcommand, not just the one
upstream supports. list --json is a verbatim relay; path/get/set/
unset/reset get cospec-owned version: 1 envelopes built from the text run;
--json against a Class B subcommand is refused with an envelope and exit 1
rather than faked. The version: 1 stamp is what makes switching to a relayed
upstream document later a version bump instead of a silent shape change.

Config precedence vs cospec's forced env keys

Where cospec's own behaviour outranks the key just written, cospec says so — on
stderr, so --json stdout stays exactly one document:

  • config set telemetry.enabled … → a note that cospec forces
    OPENSPEC_TELEMETRY=0 in WRAPPED_ENV on every wrapped call, so the setting
    affects bare openspec runs only. WRAPPED_ENV is unchanged by this PR; the
    note is the disclosure, not a new escape hatch.
  • config profile <preset> and config set profile|workflows|delivery → a note
    that cospec's harness files come from cospec canon via cospec update, not
    openspec update (which is exactly what upstream's own success line tells you
    to run).

defaultStore gets no note, because cospec genuinely honours it — as a
fallback strictly below local-root resolution. Verified end to end in the
ledger: with defaultStore set, a bare directory resolves against the named
store; a directory with a local openspec/ still wins; unset falls back to the
local cwd; a stale defaultStore fails loudly rather than silently falling
through. Every other key is annotated for nothing.

completion — native, not passed through

cospec completion [bash|zsh|fish] is generated from cospec's own exported
COMMANDS table and GLOBAL_OPTIONS, printed to stdout, with no filesystem
writes and no rc-file mutation. Shell is auto-detected from $SHELL when
omitted.

Passing upstream's generator through was rejected on principle: its installer
writes a completion function for the openspec binary into the user's shell
rc, whose dynamic completions shell out to bare openspec — a permanent
instruction in a dotfile to do the one thing this repo forbids, which no printed
warning undoes. cospec already owns a complete static command table, so
generating from it is both cheaper and more accurate.

A hidden cospec __complete <changes|specs|types> serves the dynamic slots,
emitting tab-separated id/description lines and exiting 1 silently on both
streams
on any failure — a Tab press that prints an error into the middle of a
command line is worse than one that completes nothing. types is served from
COSPEC_TYPES with no wrapped spawn at all.

The fragile part is extracting per-command flags from each entry's
pre-formatted help string, so that extraction is a pure function with a snapshot
test over the real table: a new command or flag the extractor cannot see turns
red in CI instead of quietly vanishing from completion.

feedback — files on aligned-team/cospec, --upstream relays

cospec feedback "<message>" [--body <text>] files at aligned-team/cospec via
gh issue create with array argv, shell: false, and no --label. Defaulting
to Fission-AI/OpenSpec would route cospec bug reports to a project that cannot
fix them and did not ask for them; excluding the surface would leave an
openspec command unanswered.

The native path follows upstream's shape — grapheme-aware 72-char title
truncation, gh presence and gh auth status gates, a manual-submission
fallback that prints the formatted issue plus a prefilled URL and exits 0
minus --label, whose only purpose upstream is to be retried when the repo
doesn't define the label. Dropping the flag deletes the whole failure mode. The
provenance footer names the cospec version plus the resolved wrapped-OpenSpec
source and version.

cospec feedback --upstream relays the wrapped openspec feedback verbatim
with the destination named on stderr. It uses a raw version-asserted spawn
rather than passthroughOpenspec, because upstream exits with gh's own
arbitrary status, which no expect.exitCodes allow-list can enumerate honestly
— that exception is stated in the spec rather than hidden.

Review outcome

e981e3d corrected a claim this change had been carrying as folklore. The
proposal originally asserted that upstream rejects a trailing --no-color on
every leaf but show, and used that as the reason config skips the shared
helper. Probed against the real pinned binary, it is accepted everywhere —
config's subcommands, schemas, and templates all take it. So the reason
was rewritten to what is true (it is redundant, not fatal — --store and
--json are the fatal ones), contract rows were added as the regression guard
should upstream ever change, and no follow-up fix change was proposed for a
hazard that does not exist. The same commit strengthened two tests that would
have passed without proving their claim.

The ledger pass made three further corrections rather than checking off text
that did not match what shipped: upstream prints nothing at all on a config get miss (not a message); the standalone bundling probe is feedback --json,
not feedback --help (cli.ts answers --help from the static table before it
consults COMMAND_MODULES, so --help would pass even with the module
dropped); and openspec schemas does emit --json, so omitting a schemas
completion source rests on there being no cospec slot to fill — cospec schema's
first positional is a verb — rather than on parsing fragility.

Tests

  • Unitconfig-args (35: --scope hoisting, no --store/--no-color
    across all eight subcommands with a signature proof that the builder takes no
    root at all, --json only on list, the Class A/B split, the note selector),
    feedback-format (19: grapheme-aware truncation, array argv, the two
    hardcoded destinations), completions (18: flag extraction plus a snapshot
    over the real COMMANDS table), complete-types (spawn-free).
  • Contract, against the real pinned 1.11.0 binary with XDG_CONFIG_HOME
    and HOME sandboxed into a temp dir — config-surface.test.ts (15) pins
    the trailing---no-color acceptance (with a control row proving it isn't
    special-cased), the --json-on-list-only shape, the --store rejection,
    --scope global vs upstream's own not-implemented error, and both handover
    cases.
  • Integrationconfig (19), completion (16), feedback (10). Every
    gh row stubs gh on PATH; no test touches the network.
  • Packagingpack-standalone.test.ts runs config path,
    completion zsh, and feedback --json from the compiled binary with no
    node_modules, guarding the literal-import() bundling trap that silently
    drops a command module.
  • Docs and harnessdocs:build, generate:check, agents:check all
    green; .codex/rules/cospec.rules gains the five read-only prefixes
    (config get|list|path, completion, __complete) and none of the mutating
    ones, for the same reason archive is already omitted.

Ledger: 29/33 rows verified, 4 deferred, 0 unresolved. Deferrals are 2.4 and
4.4 (need a real TTY and a human at the keyboard), 6.5 (would file a real public
issue), and 7.3 (@eval, advisory-only, no DEEPSEEK_API_KEY). Row 4.2 records
that its fish --no-execute leg did not run — fish is installed neither
locally nor in CI — while bash -n and zsh -n both pass.

Non-goals

  • init/update stay cospec-native. Passing them through would write the
    opsx files cospec's own leftover scan flags.
  • completion install/uninstall. Rc-file mutation with backups, idempotency,
    and a matching uninstaller is the bulk of upstream's completion code; docs
    ship copy-paste one-liners instead.
  • PowerShell completion.
  • Project-local config scope — upstream exits 1 with "not yet implemented";
    cospec relays that verbatim and adds nothing.
  • Re-implementing upstream's config key validation, value coercion, or its
    prototype-pollution guard. cospec never reads or writes
    ~/.config/openspec/config.json itself and grows no config file of its own.
  • Changing WRAPPED_ENV, or changing core/passthrough-command.ts's trailing
    --no-color append.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 5, 2026 23:13

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

replygirl and others added 6 commits September 8, 2026 14:44
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k tests

The contract suite's own probe shows a trailing `--no-color` is ACCEPTED on
every `openspec config` subcommand (commander resolves a program-level option
from a leaf), but proposal.md, design.md, verification.md row 1.5, the delta
spec, and tasks.md 7.6 all still asserted a rejection — and tasks.md 7.6 seeded
a follow-up `fix` change on that falsified premise. Correct every artefact to
the observed behaviour, probe the flag without a leading `--no-color` in the
argv (so the probe is about the trailing copy alone), and add a row proving
`schemas`/`templates` accept it too, which settles the supposed hazard on the
shared passthrough helper instead of handing it on.

Also fix two tests that asserted less than they claimed:

- pack-standalone ran `cospec feedback --help`, which `cli.ts` answers from the
  static COMMANDS table before it ever looks up COMMAND_MODULES — it passes
  even when the module was dropped from the compiled binary, the exact
  regression the row exists to catch. Run the command itself with `gh` stripped
  from PATH (manual-submission fallback, exit 0, `submitted: false`).
- The `--upstream` gh-failure row only asserted a non-0/non-1 exit; upstream
  does `process.exit(error.status ?? 1)`, so assert the exact code (42).
Adds cospec config's machine-global section (call classes, --json envelope
shapes per subcommand, --store refusal, defaultStore cross-link) to
reference/configuration.md, three command rows plus the config exceptions
note to reference/commands.md, per-shell completion snippets to
guide/installation.md, the config passthrough exceptions and the named
terminal-handover class to docs/architecture.md, and the config/completion/
feedback mention to how-it-relates-to-openspec.md, README.md, and
apps/cli/README.md. Updates .agents/shared.md's route-through-cospec bullet
and re-syncs CLAUDE.md/AGENTS.md via mise run agents:sync. Checks off
tasks.md 7.1-7.5 and verification.md 8.1-8.2 with observed mise run
docs:build / agents:sync / agents:check results; 8.3 stays for the human
manual-read row.
Re-ran every named command on the final branch state and recorded the
observed result on each verification row: unit (732 pass), contract (50
pass), integration (158 pass), pack (2 pass), pack:standalone (2 pass),
docs:build, generate:check, agents:check, and direct observations for the
rows no shipped test covers (defaultStore precedence end to end, EDITOR/
VISUAL unset relay, config profile --json refusal, the profile preset
note, and the --store/no-subcommand refusals).

Four rows are deferred with reasons: 2.4 and 4.4 (need a real TTY), 6.5
(would file a real public issue), and 7.3 (@eval, no DEEPSEEK_API_KEY).
Row 4.2 records that its fish leg did not run — fish is installed neither
here nor in CI.

Three claims are corrected to what shipped rather than checked off as
written: 1.4 (upstream prints nothing on a config get miss, not a
message), 7.1 (the standalone bundling probe is feedback --json, not
--help, and lives in test:pack:standalone), and 7.4 (mise run check has
two non-green steps, both pre-existing on main, both outside CI's gate).
tasks.md 1.2 and design.md are corrected likewise: openspec schemas does
emit --json, so the omission of a schemas completion source rests on
there being no cospec slot to fill, not on fragility.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cospec archive validated the change, passed both hard gates
(archive/verification-incomplete, archive/scenario-preservation) with no
--force, delegated to openspec archive, verified the move on disk, and
fanned blocker sync out. Specs: +11 ~1 -0, all applied and verified.

Three new capability specs land (openspec-config-passthrough,
cospec-shell-completion, cospec-feedback) and openspec-read-passthroughs
gains the terminal-handover class shared by workset open and
config edit|profile|reset. Each new spec gets a real ## Purpose in place
of the archive-generated placeholder, which the specs/purpose-tbd rule
correctly refused to let through.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@replygirl
replygirl force-pushed the worktree-openspec-passthroughs branch from f2c9802 to 953319e Compare September 8, 2026 19:47
@replygirl
replygirl merged commit 0a76b9e into main Sep 8, 2026
11 checks passed
@replygirl
replygirl deleted the worktree-openspec-passthroughs branch September 8, 2026 19:51
replygirl added a commit that referenced this pull request Sep 9, 2026
oxfmt column-aligns the merged config/completion/feedback rows (from
#35) with the init/update/doctor rows (agents harness additions from
this branch); the hand-resolved conflict left them misaligned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
replygirl added a commit that referenced this pull request Sep 9, 2026
…/skills (#36)

* feat(harness): add the shared .agents/skills root and an agents target

Introduce a body-dialect layer in the harness renderer so a skill body can
be respelled per target, and move the `codex` target's skills from
`.codex/skills/` to the vendor-neutral `.agents/skills/` root. Add a fourth
harness, `agents`, which writes that root byte-identically to `codex`
(`codex` differs only by also emitting `.codex/rules/cospec.rules`), so
`--harness all` now means four targets. Two harnesses mapping one path to
different bytes is a hard render error instead of last-writer-wins.

`init` and `update` migrate an existing `.codex/skills` install: files whose
contents still hash-match the generated output are removed, hand-edited
copies are left in place until `--force`, and `.codex/` itself is never
removed. Both commands report the migration in `--json` under a top-level
`migration` key; `update --check` counts a remaining legacy layout as drift
and `doctor` reports it as the `legacy-layout` warning.

Regenerate this repo's own skills into `.agents/skills/` and move the
formatter exemption with them — the shared-dialect bodies exceed the 80-col
markdown proseWrap, so without the `.prettierignore` move `format:fix` and
`generate:check` become mutually unsatisfiable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* test(harness): cover the shared dialect, codex/agents byte-identity, and migration

Retarget the adapter tests at `transformBody(body, dialect, skillById)` and
assert an unknown skill id is left verbatim so doctor still flags a real
dangling reference. Add render coverage for the `agents` file set, the
byte-identity of the `codex` and `agents` roots (contentHash included), the
first-selected-harness ownership rule for a shared path, and a real dedupe
guard that flips a copied canon's dialect and asserts the render throws.
Extend the dangling-ref sweep to resolve a slash token as a workflow id or a
`cospec-<suffix>` skill name, mirroring doctor's two-way resolution. New
suite for the legacy `.codex/skills` migration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: document the shared .agents/skills root and the agents harness target

Record the new `--harness agents` value and the four-target meaning of
`all`, the byte-identity of the `codex` and `agents` roots, the
`$cospec-<skill>` / `/cospec-<skill>` invocation spelling keyed on the skill
directory name rather than the workflow id, `.agents/skills` (not a bare
`.agents/`) as the auto-detection key, the `.codex/skills` migration and its
`--force` behaviour, and doctor's `legacy-layout` warning counting as drift.
Remove the now-false claim in docs/harness-integration.md that cospec never
writes under `.agents/`, and re-sync the shared agent-doc block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(harness): stop a codex-only install detecting as codex+agents

`codex` and `agents` render the same `.agents/skills` tree, so the tree
alone cannot say which target was selected. Detection had a marker guard
for `codex` (`.codex/rules/cospec.rules`) but none for `agents`, so every
codex repo reported `agents` too the moment it migrated off
`.codex/skills` — inventing a target the user never chose in the one
place that records what they opted into.

A marker-less harness is now reported only when no detected
marker-bearing harness already accounts for the same skill base. A repo
that really selected both re-generates as `codex`, which writes a strict
byte-for-byte superset, so nothing is lost.

Tests: detectHarnesses across legacy-only, migrated, agents-only, both,
four-target and bare-`.agents/` trees, each proved a detect->generate->
detect fixpoint. Doctor gains coverage for `legacy-layout`, the shared
dialect's two-way skill/workflow reference resolution, and the deduped
`.agents` / `.agents/skills` walk (all three fail under mutation).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs: mention the shared .agents/skills root in the CLI package README

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(harness): author harness-agents-target change

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat(harness): archive harness-agents-target

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(hooks): exclude the generated .agents root from oxfmt batches

.prettierignore already lists .agents/skills/cospec-*/ as generator-owned
(the shared vendor-neutral skills root the codex/agents targets write), but
hk's oxfmt step only excluded .claude/**, .codex/**, and .opencode/** from
its file list. Any push or commit touching those SKILL.md files handed oxfmt
a batch of nothing but ignore-excluded files, which it reports as
`Expected at least one target file` and exits non-zero — the same latent-hook
class fixed for the vendored openspec bundle in 206d965.

Add .agents/skills/** (not the broader .agents/**, since .agents/shared.md is
hand-authored and must stay in oxfmt's check) to generatedOutputGlobs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* style: reformat commands.md table after rebase conflict resolution

oxfmt column-aligns the merged config/completion/feedback rows (from
#35) with the init/update/doctor rows (agents harness additions from
this branch); the hand-resolved conflict left them misaligned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <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.

2 participants