feat(skill): bcli skill install — slash command projection (AIP Phase 6/7)#20
Conversation
igor-ctrl
left a comment
There was a problem hiding this comment.
Lead review — REQUEST_CHANGES (integration refactor only; substantive content is solid)
The skill-install machinery is excellent — args inference from params: is the right backward-compat call, the no-generated_at idempotency is exactly the right defensive choice (avoids the same churn pitfall I flagged on PR #19's SKILL.md frontmatter), and the test suite covers every contract clause. 811 passed / 5 skipped / ruff clean / CI green on 3.11/3.12/3.13 / Mergeable=CLEAN.
But there's a real skill Typer group ownership collision with PR #19 that needs resolving before merge.
The collision
Both PR #19 (already APPROVED) and this PR register name="skill" via app.add_typer(...):
PR #19 src/bcli_cli/app.py:
app.add_typer(skill_init_cmd.app, name="skill", help="Generate a per-user bcli skill bundle (AIP Phase 7)")PR #20 src/bcli_cli/app.py:
app.add_typer(
skill_cmd.app,
name="skill",
help="Project saved queries + batch templates as Claude Code slash commands (AIP §Phase 7)",
)Whichever lands second will text-conflict on this region, AND Typer's add_typer errors on duplicate group names — only one app.add_typer(..., name="skill", ...) call can exist.
Both PRs assumed the other would integrate around their group. PR #20's body even says "bcli skill is registered as a Typer GROUP so Worker B's Phase 7 bcli skill init wizard can land alongside without restructuring" — but PR #19 made the same assumption in reverse.
Resolution: Option A — PR #19 keeps the skill group; this PR adapts
Rationale:
- PR #19 was reviewed/approved first in this session; revising it would reopen a closed review loop.
- The
init/updatewizard is the broader Phase 7/8 mechanism — natural home for the group label. installis one operation that fits naturally inside that group.- The required change in this PR is small.
Asked change (~5 lines):
-
src/bcli_cli/commands/skill_cmd.py:- Remove
app = typer.Typer(...)constructor call. - At the top, add:
from bcli_cli.commands import skill_init_cmd. - Replace
app = typer.Typer(...)with:app = skill_init_cmd.app # share the ``skill`` Typer group with the wizard (Phase 7 mechanism). - Keep the
@app.command("install")decorator oninstall_command— it now lands inside the shared group. - The
__all__ = ["app", "install_command"]export stays; downstream tests can stillrunner.invoke(skill_cmd.app, ["install", ...])against the same Typer instance.
- Remove
-
src/bcli_cli/app.py:- Remove the new
app.add_typer(skill_cmd.app, name="skill", ...)block. PR #19 already registers the group; importingskill_cmdin this module is enough — its@skill_init_cmd.app.command("install")decorator runs at import time and attachesinstallto the existing group. - Add
skill_cmdto the import list at line 174 (the multi-linefrom bcli_cli.commands import (...)block) so the decorator is triggered.
- Remove the new
After the refactor, bcli skill --help lists init, update, AND install — one group, three commands, no collision.
The substantive skill_cmd.py content (everything below the Typer constructor) doesn't need to change.
Substantive review (would be APPROVE absent the collision)
Args inference from params: (review focus #1) ✓
_coerce_args correctly preserves explicit args: when present, otherwise walks params: keys with required-first then optional-second, both in YAML insertion order (Python 3.7+ dict iteration is insertion-ordered). test_skill_install_args_inferred_from_params_when_omitted seeds a query with required: true first, two default: params after, and asserts the resulting argument-hint reflects that ordering. Existing saved-query bundles that don't carry args: get usable slash commands with zero hand-migration. Strong backward-compat win.
No-timestamp idempotency (review focus #2) ✓
test_skill_install_idempotent_no_changes is rigorous: captures mtime_ns after first run, sleeps 50ms for filesystem granularity, runs again, asserts mtime_2 == mtime_1. If the second run rewrote — even atomically — mtime would advance. Per-file content hash (content_hash: sha256:... in the provenance comment) gates rewrites in install_command via _existing_hash() == _content_hash_of(rendered). Combined with the no-generated_at design, re-runs on unchanged sources are byte-stable no-ops. test_skill_install_embedded_content_hash_is_stable independently pins that the embedded hash matches the body's hash, so external sha256sum'ing the file post-install would match.
manual: true YAML frontmatter (review focus #3) ✓
_is_manual parses the YAML frontmatter between --- markers via regex + yaml.safe_load, then checks loaded.get("manual", False). test_skill_install_preserves_manual_files writes a manual file, runs install, asserts the file is untouched and the install summary reports it. The switch from comment-based to YAML-frontmatter detection is the right call — frontmatter is the conventional Claude Code metadata location and survives a bcli skill update re-run cleanly.
Target resolution (review focus #4) ✓
_resolve_target_dir: explicit --target always wins → CWD with .claude/ → $HOME. test_skill_install_target_dir_defaults_to_cwd_when_dot_claude_present pins the second clause; the third clause is exercised implicitly via the other tests (which use --target to escape $HOME pollution). Worth one more test pinning the third clause explicitly, but non-blocking.
Batch template discovery (review focus #5) ✓
_discover_batches walks ~/.config/bcli/batches/<profile>/*.yaml. test_skill_install_creates_command_per_batch_yaml confirms a discovered batch becomes a slash command with the right body (bcli batch run <path> --result-out ... invocation). CWD-relative discovery deferral correctly documented in PR body.
docs/saved-queries.md updates (review focus #6) ✓
New ### Slash-command projection (Phase 6) section added describing the schema extension (description, categories, args with params: fallback documented) and command output shape. Cross-references both _coerce_args semantics and the manual-frontmatter override.
Additional notes (non-blocking)
_render_command_mdbuilds the file in chunks via string concatenation. Stdlib-only, no jinja2 dependency — clean._summariseprints to Rich stdout. For pipe-redirected callers, the summary still goes to stdout — could be moved to stderr like the batch ledger metadata (Phase 3) for consistency, but acceptable for a one-shot wizard.- Atomic writes use
tempfile.mkstemp+os.replace(same pattern as_envelope_wrapand_atomic_writein PR #19) — consistent module-level idiom across the codebase. - The
<!-- content_hash: sha256:... -->HTML comment appended to SKILL.md and the file-level provenance comment is the same pattern PR #19 uses (YAML frontmatter for SKILL.md, inlineprovenance:block per query). Two different formats per file type but each appropriate to the consumer (Claude Code reads YAML frontmatter from SKILL.md; commands files use HTML comments for arbitrary metadata).
What to push next
skill_cmd.py: replaceapp = typer.Typer(...)withfrom bcli_cli.commands import skill_init_cmd; app = skill_init_cmd.app.app.py: remove theapp.add_typer(skill_cmd.app, name="skill", ...)block; ensureskill_cmdis in the import list so the@app.command("install")decorator runs.- Verify locally:
uv run bcli skill --helplistsinit,update,install;uv run bcli skill install --helpworks; all 12test_skill_installtests still green. - Re-push. I'll re-review on next ping.
Verdict: REQUEST_CHANGES on the integration refactor only. Substantive work is solid. Worker A — apologies the heads-up about PR #19's group registration didn't reach you in time.
82357b4 to
310e9ca
Compare
|
Refactor applied per the review — chose rebase on top of PR #19 for a clean fast-forward post-merge. Two-file delta (squashed into the existing commit via rebase, new SHA
Verified locally:
Branch was force-pushed ( Ready for re-review. |
…e 7)
Generate Claude Code slash commands from saved queries + batch templates.
New ``bcli skill install [--target PATH] [--dry-run]``:
- Reads the active profile's saved queries from
``~/.config/bcli/queries/<profile>.yaml`` and batch templates from
``~/.config/bcli/batches/<profile>/*.yaml``.
- Emits one ``.claude/commands/bcli-<name>.md`` per item with frontmatter
(description, argument-hint), a usage section, and a body that
invokes ``bcli q <name> arg=$1 …`` or ``bcli batch run <yaml> …``.
- Emits a top-level ``.claude/skills/bcli/SKILL.md`` index grouped by
``categories:``.
- Idempotent via SHA-256 content hash embedded in the provenance
comment; re-runs on unchanged sources are no-ops (mtime preserved).
No ``generated_at`` timestamp — would invalidate the hash on every
run and cause infinite churn.
- ``manual: true`` in a file's YAML frontmatter protects it from
regeneration even if a saved query by the same name exists.
- Target resolution: explicit ``--target`` wins; else CWD when it has
a ``.claude/`` dir; else ``$HOME``. Matches Claude Code's per-project
vs global skill convention.
- Atomic writes (.tmp + os.replace) for files that change.
``bcli skill`` is registered as a Typer GROUP so Worker B's Phase 7
``bcli skill init`` wizard can land alongside without restructuring.
Saved-query YAML schema extension (additive, backward-compatible):
- ``description``: already supported.
- ``categories: [str]``: new; falls back to ``["unsorted"]``. Used to
group commands in the SKILL.md index.
- ``args: [{name, type, required, example}]``: new; explicit positional
ordering for the generated slash command. **If omitted, derived from
``params:`` keys** (required first, optional with ``default:`` second,
both in YAML insertion order). Most existing queries need zero
changes — their slash command works on the inferred ordering.
Batch template discovery is keyed to ``~/.config/bcli/batches/
<profile>/*.yaml`` per the file conventions established elsewhere.
CWD-relative discovery deferred to v0.2 (open issue if needed).
Stdlib only — string templates, no jinja2.
Tests: +12 in tests/test_skill_install/test_skill_install.py covering
per-query and per-batch generation, args inference, argument-hint
rendering, skill index grouping/sorting, idempotency on unchanged
sources, regeneration on source changes, manual-frontmatter
preservation, dry-run, content-hash integrity, target-dir resolution
(CWD detection), and empty-input no-op.
docs/saved-queries.md updated with the schema extension and a new
"Slash-command projection" section.
Suite: 799 → 811 passed. Ruff clean. Hermetic (HOME=/tmp/empty):
clean across tests/test_skill_install/ tests/test_describe/
tests/test_idempotency/.
310e9ca to
7883a33
Compare
|
Rebased on New branch shape: One commit on top of main; Verified locally:
PR status: |
igor-ctrl
left a comment
There was a problem hiding this comment.
Lead re-review of 310e9ca — APPROVE
Refactor delta is exactly what I asked for. The substantive 592-line bcli skill install implementation is preserved unchanged; only the Typer-group integration is rewired.
Verification
Alias pattern in skill_cmd.py:
# `bcli skill` is a single Typer group shared by ``skill_init_cmd``
# (``init`` / ``update`` wizards — PR #19) and this module
# (``install``). PR #19 was approved first and owns the group's
# top-level registration in ``app.py``; we alias its Typer instance
# so ``@app.command("install")`` below attaches to the *same* group.
# This avoids a duplicate ``add_typer(..., name="skill", ...)`` (which
# Typer raises on) and keeps ``bcli skill --help`` listing init,
# update, and install side by side.
from bcli_cli.commands import skill_init_cmd # noqa: E402
app = skill_init_cmd.appComment cites PR #19, explains the rationale (Typer raises on duplicate group names), and the alias keeps the rest of the module unchanged. The 592 lines below this stay-as-they-were.
app.py:
skill_cmdadded to the multi-linefrom bcli_cli.commands import (...)block with# noqa: F401 — import-time side effect registers \skill install`` — exactly the import-only-for-decorator-side-effect pattern.- Explanatory comment introducing the
skill_init_cmd.appregistration block ("skillis a single Typer group shared byskill_init_cmd(init / update) andskill_cmd(install). PR #19 owns the group registration; theskill_cmdmodule aliasesskill_init_cmd.app..."). - Exactly one
app.add_typer(... name="skill" ...)call (PR #19's). Noapp.add_typer(skill_cmd.app, name="skill", ...).
bcli skill --help (in /tmp/aip-review-20-refactor):
Commands
init Interview the user and generate a per-user bcli skill bundle.
update Re-run the wizard against the current describe surface.
install Project saved queries + batch templates as Claude Code slash
commands.
Three commands, one group. No duplicate-group error at import. Clean.
Test suite + lint:
uv run pytest tests/→ 831 passed, 5 skipped, 3 warnings in 21.31s (matches worker self-report: 811 Phase 6 baseline + 20 absorbed from PR #19 via rebase).uv run ruff check src/ tests/→ All checks passed!
GitHub state:
headRefOid: 310e9caa67fc8b20f979aa0af1113be0a4eb57c2matches Worker A's reported tip.- CI: SUCCESS on 3.11/3.12/3.13.
mergeable: CONFLICTING / mergeStateStatus: DIRTYis the expected state: this branch is rebased ontoorigin/feat/skill-init-mechanism(PR #19), so GitHub sees a dependency on commits not yet in main. Once Igor merges PR #19 (already APPROVED), this branch fast-forwards cleanly. Worker A flagged this in the self-report.
Rebase-vs-import-from-future choice
Worker A chose rebase over the "import from a future state" pattern. Both are valid; rebase has the advantage of clean fast-forward semantics post-merge of #19, with the trade-off of force-push-induced CI re-runs. Reasonable engineering call.
Substantive content (from prior review, unchanged)
All my prior APPROVE-grade observations on the substantive Phase 6 work stand:
args:derivation fromparams:(required-first then optional, YAML insertion order) — strong backward-compat win.- No-
generated_atidempotency with SHA-256 content hash;mtime_nsinvariance test confirms byte-stable re-runs. manual: trueYAML frontmatter protects hand-edits.--targetresolution: explicit > CWD-with-.claude>$HOME.- Stdlib only — no jinja2.
- 12 tests pinning every contract clause.
docs/saved-queries.mdupdated with the schema extension.
After this merges
bcli 0.4.0 is ready to tag. PR #19 merges first → PR #20 fast-forwards → version bump + CHANGELOG + tag. The release plan in tasks/todo.md is being extended with Phase 6 + Phase 7 (skills mechanism) inline now.
Verdict: APPROVE. Igor merges; I do not.
igor-ctrl
left a comment
There was a problem hiding this comment.
Post-merge-of-#19 follow-up — APPROVE still stands at 7883a33
Saw the orchestrator's heads-up that #19 has merged to main and you rebased feat/skill-install onto origin/main (new tip 7883a33, drops 6258aa3 as "already applied"). Re-verified on the new tip:
git log origin/main..origin/feat/skill-install --onelineshows exactly one commit (7883a33) — Phase 6 work as a clean delta on merged-#19 main.- Diff stat unchanged: 5 files, +1163 / -1.
- Local on
7883a33: 831 passed, 5 skipped, ruff clean in 21.65s. bcli skill --helpstill shows init / update / install together under one group.- GitHub:
mergeable: MERGEABLE,mergeStateStatus: CLEAN, CI SUCCESS on 3.11 / 3.12 / 3.13.
The substantive refactor delta is byte-equivalent to what I just APPROVE'd at 310e9ca — rebase only changed commit hashes. Verdict still: APPROVE at 7883a33. Igor merges.
After this lands, 0.4.0 is ready to tag.
Implements
tasks/todo.mdPhase 6 /agent-cli-contract-v0.1.md§Phase 7 — slash-command projection from saved queries and batch templates.Summary
bcli skill install [--target PATH] [--dry-run]projects the active profile's saved queries and batch templates as Claude Code slash commands..claude/commands/bcli-<name>.mdper saved query; one.claude/commands/bcli-batch-<name>.mdper batch template at~/.config/bcli/batches/<profile>/*.yaml..claude/skills/bcli/SKILL.mdindex grouped bycategories:(matches Claude Code's documented~/.claude/skills/<name>/SKILL.mdconvention).manual: truein a file's YAML frontmatter protects it from regeneration — even if a saved query by the same name exists.bcli skillis registered as a Typer group so Worker B's Phase 7 wizard (bcli skill init) can land alongside without restructuring.Saved-query YAML schema extension (additive)
Three new optional fields, all backward-compatible:
descriptiondescription:categories["unsorted"]SKILL.mdargsparams:keysKey migration trap addressed: when
args:is omitted, the installer derives it fromparams:keys — required first, optional (withdefault:) second, both in YAML insertion order. Existing saved-query bundles get usable slash commands without hand-editing.docs/saved-queries.mdupdated with the schema extension + a new "Slash-command projection" section covering idempotency,manual: true, dry-run, and--targetresolution.Coordination with Worker B (Phase 7)
app.add_typer(skill_cmd.app, name="skill")); Worker B addsskill initinside without restructuring.args:shape unchanged.Decisions worth flagging
generated_attimestamp — would invalidate the content hash on every run and cause infinite churn. The file's mtime tells you when it was generated.manual: trueis YAML frontmatter only, not a comment — single parse path, one canonical convention.~/.config/bcli/batches/<profile>/*.yamlonly. Open an issue if needed for project-checked-in batch workflows.string.Template-equivalent inline), no jinja2.Test plan
uv run pytest tests/test_skill_install/ -v— 12 tests, all greenuv run pytest tests/ -v— 811 passed, 5 skipped (was 799 baseline; +12 new)HOME=/tmp/empty uv run pytest tests/test_skill_install/ tests/test_describe/ tests/test_idempotency/— hermetic 47 passeduv run ruff check src/ tests/— cleanbcli skill --helpshows the group;bcli skill install --helpshows the subcommandbcli describe skill install --format jsonauto-picks up the new command (effects:["other"], options:["--target", "--dry-run"])