Skip to content

#141: SignalForge skill + install-skill - #166

Merged
wjduenow merged 25 commits into
devfrom
feature/141-claude-skill-install
May 30, 2026
Merged

#141: SignalForge skill + install-skill#166
wjduenow merged 25 commits into
devfrom
feature/141-claude-skill-install

Conversation

@wjduenow

@wjduenow wjduenow commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Super-plan for #141 — ship a SignalForge Claude Code skill (src/signalforge/skills/signalforge/SKILL.md) bundled in the wheel, plus a signalforge install-skill CLI subcommand that drops it into a user's project at .claude/skills/signalforge/.

Phase: detailing (awaiting approval)
Stories: 9 implementation + Quality Gate + Patterns & Memory = 11
Decisions: 24 captured (DEC-001 … DEC-024)

What's locked

  • Architecture: install-skill mirrors init-demo verbatim — public lib seam at signalforge.skill.install_skill(...) with three typed errors (SkillDestPathError/SkillDestUnsafeError/SkillPackageDataMissingError); thin CLI handler wraps to CliInstallSkill* per cli-layer.md's library-surface pattern.
  • Destination policy: always overwrite SKILL.md + the files we ship; preserve every other file in .claude/skills/signalforge/. No --force flag in v0.1.
  • Symlink defence: ports copy_demo's three-trap pattern (RuntimeError ≤3.12 + OSError(ELOOP) ≥3.13 + is_symlink() refuse-on-overwrite).
  • e2e demo paths: zero-credential (init-demogenerate) is always-on; live pytest -m e2e is opt-in behind explicit confirmation + env-var check + cost warning.
  • Two-name convention: package-data at src/signalforge/skills/ (plural, matches install destination); Python lib at src/signalforge/skill/ (singular, matches signalforge.demo).
  • Enforcement gates: wheel_smoke pins the file set (DEC-011); AST scan Quality grader: rubric scoring of surviving artifacts #7 picks up signalforge/skill/errors.py automatically (bump 12→13); NEW tests/cli/test_skill_cli_parity.py scans the live argparse subparser registry + key demo commands + the install-skill bootstrap line against SKILL.md; 5-surface parity for the subcommand itself.
  • Self-grade: pre-release manual clauditor grade run; pin score in assets/SKILL.eval.json; shields.io badge from pinned endpoint. No CI integration; no Anthropic key in repo secrets.

Story map

  1. US-001 — Bootstrap src/signalforge/skills/signalforge/ tree + wheel packaging + wheel_smoke extension
  2. US-002 — Public signalforge.skill lib + typed errors + AST scan Quality grader: rubric scoring of surviving artifacts #7 bump (TDD)
  3. US-003 — CLI install-skill subcommand + exit-code mapping + subprocess --help smoke (TDD)
  4. US-004 — SKILL ↔ CLI parity gate (NEW test)
  5. US-005 — 5-surface parity for install-skill
  6. US-006 — Docs (docs/skills.md + mkdocs.yml nav + docs/cli-ops.md + README pointer)
  7. US-007 — Author the SKILL.md prose (DEC-020 frontmatter + DEC-021 seven sections)
  8. US-008 — clauditor self-grade + README badge
  9. US-009.claude/rules/skill-parity.md + cli-layer.md parity-surface update (orchestrator-only writes)
  10. US-010 — Quality Gate (code-review ×4 + CodeRabbit + full validation)
  11. US-011 — Patterns & Memory

Plan document

See plans/super/141-claude-skill-install.md for the full 24-DEC plan with risks and non-goals.

Next steps

  • Review the decisions in this PR
  • Approve in Claude Code (say "approved" or "ready for Ralph") to devolve to beads
  • Ralph picks up US-001 first (no deps); rest follow the dependency graph

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an install-skill command to install the bundled Claude Code skill and bumped package version.
  • Documentation

    • New comprehensive skill docs, README quick-start update, and site nav entry describing installation, workflows, demos, and troubleshooting.
  • Tests

    • New parity, smoke, unit, and packaging tests to ensure CLI help, docs, demo commands, and bundled skill assets stay consistent.
  • Chores

    • Packaging updated to include bundled skill assets and evaluation metadata.

Review Change Stack

Phase 4 detailing complete. 24 decisions locked across:
- skill source path (`src/signalforge/skills/signalforge/` package-data tree;
  `src/signalforge/skill/` Python lib)
- destination policy (always overwrite SKILL.md, preserve siblings, no --force)
- symlink/cycle defence (mirrors copy_demo verbatim)
- error hierarchy (SkillError base + 3 concretes; spans tiers 1+2 → excluded base)
- wheel packaging + wheel_smoke gate
- AST scan #7 bump (12→13)
- SKILL ↔ CLI parity gate (new test scans live argparse + key demo commands)
- 5-surface parity for install-skill
- self-grade ops (pre-release manual; pinned in eval.json + README badge)
- e2e demo paths (zero-cred default + opt-in live)
- skill-parity.md rule + cli-layer.md update (orchestrator-only)

11 stories laid out: US-001…US-009 implementation + US-010 Quality Gate + US-011
Patterns & Memory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da14a076-2e75-4cae-95fb-ec5f1f344c01

📥 Commits

Reviewing files that changed from the base of the PR and between f8fb531 and bd8fd31.

📒 Files selected for processing (7)
  • plans/super/141-claude-skill-install.md
  • src/signalforge/cli/install_skill.py
  • src/signalforge/skill/__init__.py
  • src/signalforge/skills/signalforge/SKILL.md
  • src/signalforge/skills/signalforge/assets/SKILL.eval.json
  • tests/cli/test_skill_cli_parity.py
  • tests/skill/test_install.py
💤 Files with no reviewable changes (1)
  • plans/super/141-claude-skill-install.md
✅ Files skipped from review due to trivial changes (2)
  • src/signalforge/skills/signalforge/assets/SKILL.eval.json
  • src/signalforge/skills/signalforge/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/signalforge/cli/install_skill.py
  • tests/cli/test_skill_cli_parity.py

📝 Walkthrough

Walkthrough

Implements GH #141: bundles a Claude Code skill, adds library API install_skill and install-skill CLI, enforces SKILL.md↔CLI parity via tests, updates packaging to include skill assets, and adds docs, plan, and exit-code wiring.

Changes

Claude Code Skill Installation

Layer / File(s) Summary
Skill error hierarchy and library install API
src/signalforge/skill/errors.py, src/signalforge/skill/__init__.py
SkillError base and concrete Skill* errors; install_skill(dest) resolves/validates destination (symlink-cycle, unsafe targets), locates packaged skill via importlib.resources, copies files with overwrite semantics, and returns installed SKILL.md path.
CLI error wrappers and exit-code mapping
src/signalforge/cli/errors.py, src/signalforge/cli/_helpers.py
Adds CliInstallSkillPathError, CliInstallSkillDestUnsafeError, CliInstallSkillPackageDataMissingError with remediation defaults and registers Skill*/CliInstallSkill* types in _EXCEPTION_TO_EXIT_CODE (Tier 1 and Tier 2).
CLI install-skill subcommand
src/signalforge/cli/install_skill.py, src/signalforge/cli/__init__.py
add_parser() registers install-skill with optional dest (default "."), cmd_install_skill() pre-probes for existing SKILL.md, delegates to signalforge.skill.install_skill, wraps/prints errors, emits one INFO line on success (optional “(replaced existing SKILL.md)”) and returns mapped exit codes.
Bundled Claude Code skill
src/signalforge/skills/signalforge/SKILL.md, src/signalforge/skills/signalforge/assets/SKILL.eval.json
SKILL.md documents installation, demos, workflows, parity expectations, e2e gating, troubleshooting, and exit-code taxonomy; SKILL.eval.json contains pre-release grading metadata.
Unit tests for install_skill library
tests/skill/test_install.py
Unit tests cover happy path, overwrite behavior, symlink refusals (SKILL.md and ancestor), assets symlink refusal, mkdir failure wrapping, symlink-cycle detection, missing package-data, dest-as-file rejection, SkillError str behavior, non-ELOOP OSError propagation, and strict=False fallback.
CLI handler tests
tests/cli/test_install_skill.py
End-to-end CLI handler tests: success/resolved path stdout, overwrite suffix, tiered error mapping, default dest (CWD), help surface, future-exception coverage, KeyboardInterrupt propagation, constructor/cause rendering, and regression swallowing OSError in pre-probe.
Skill↔CLI parity gate and integration tests
tests/cli/test_5_surface_parity_install_skill.py, tests/cli/test_skill_cli_parity.py, tests/cli/test_subprocess_smoke.py, tests/cli/test_exit_codes.py
Parity tests build the live parser, assert registered subcommands and flags appear in SKILL.md, check canonical demo/install lines, include a planted-violation self-test, and provide 5-surface coverage plus subprocess smoke and exit-code fixture updates.
Wheel packaging, audit, and config
pyproject.toml, tests/test_wheel_packaging.py, tests/test_audit_completeness.py, src/signalforge/__init__.py
Adds clauditor-eval dev dependency; expands wheel include to bundle src/signalforge/skills; wheel-smoke tests assert expected skill files and exclude .claude/skills/; test_audit_completeness updated for SkillError and module count; version bumped to 0.5.0.dev0.
Documentation, plan, and orchestrator rules
plans/super/141-claude-skill-install.md, .claude/rules/skill-parity.md, .claude/rules/cli-layer.md, docs/skills.md, docs/cli-ops.md, mkdocs.yml, README.md
Adds full implementation plan, skill-parity rule, cli-layer parity note, docs for skill installation and CLI ops, MkDocs nav entry, README badge/quickstart linking to install-skill and clauditor eval asset.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

"A rabbit's note on install delight,
SKILL.md tucked in wheel so tight,
Symlinks shunned, parity kept true,
Tests watch tokens, docs guide you,
Hopping forward — signalforge new!" 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "#141: SignalForge skill + install-skill" directly and clearly summarizes the main change: shipping a SignalForge CLI skill plus a new install-skill subcommand, matching the PR's core objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

wjduenow and others added 2 commits May 29, 2026 22:22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

wjduenow and others added 19 commits May 30, 2026 10:23
Approved + devolved. US-001 is at the front of the ready queue.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lays down src/signalforge/skills/signalforge/{SKILL.md,assets/SKILL.eval.json}
as structural placeholders. Wires [tool.hatch.build.targets.wheel].include for
the skills tree. Extends wheel_smoke with _EXPECTED_SKILL_FILES (positive) and
a negative assertion that no .claude/skills/* paths appear in the wheel.

Plan: plans/super/141-claude-skill-install.md US-001 / DEC-001, 010, 011, 022.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… AST scan #7 bump

Adds signalforge.skill subpackage with install_skill(dest) -> Path and the
three-class typed-error hierarchy (SkillDestPathError tier 1, SkillDestUnsafeError
tier 2, SkillPackageDataMissingError tier 1). Mirrors signalforge.demo.copy_demo
verbatim for symlink-cycle defence and importlib.resources lookup; never rmtree.

Registers the three lib concretes in _EXCEPTION_TO_EXIT_CODE; adds SkillError to
_EXCEPTION_MAPPING_EXCLUDED_BASES; bumps AST scan #7 count 12 -> 13.

Plan: plans/super/141-claude-skill-install.md US-002 / DEC-002, 003, 005, 006, 007, 008, 009.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + exit-code mapping + subprocess smoke

Adds 'signalforge install-skill [<dest>]' wired via add_parser/cmd_install_skill
mirroring init_demo shape. Three CliInstallSkill*Error wrappers (Path/DestUnsafe/
PackageDataMissing) registered in _EXCEPTION_TO_EXIT_CODE (tier 1/2/1). Stdout
success line + (replaced existing SKILL.md) on overwrite per DEC-017. Subprocess
--help smoke under cli_subprocess marker. Per-class construction branches added
to test_exit_codes.py for the three new wrappers.

Plan: plans/super/141-claude-skill-install.md US-003 / DEC-002, 003, 004, 008, 009, 017, 024.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…andler + exit-code mapping + subprocess smoke
… body sections)

Replaces the US-001 placeholder with the real user-facing workflow per DEC-020
(frontmatter) and DEC-021 (seven sections). Body teaches the full pipeline:
point-at-project, zero-cred demo, real draft+prune, prune-existing,
diff-reading, gated live e2e, troubleshooting. Contains every canonical token
the US-004 parity gate scans for (subcommand names + demo command lines +
install-skill bootstrap).

Plan: plans/super/141-claude-skill-install.md US-007 / DEC-012, 013, 020, 021.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NEW tests/cli/test_skill_cli_parity.py scans src/signalforge/skills/signalforge/
SKILL.md for three token categories per DEC-015:
1. Every subcommand name from the live argparse parser (auto-grows)
2. Four canonical demo command lines (signalforge init-demo / generate <model>
   --write / prune-existing <model> --schema <path> / install-skill)
3. The install-skill bootstrap line (covered by category 2's fourth entry)

Plain substring match; no normalisation. Planted-violation self-check proves
the gate can fail loud — per testing-signal.md AST-source-scan-gate philosophy.

The gate runs inside the canonical VALIDATE_CMD (uv run pytest) so /ralph-run
keeps the skill current automatically without relying on the model remembering.

Plan: plans/super/141-claude-skill-install.md US-004 / DEC-015, 016, 019.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the anticipatory skill-parity.md with the post-implementation
contract: names the actual shipped artefacts (src/signalforge/skill/,
src/signalforge/skills/signalforge/SKILL.md, signalforge install-skill,
tests/cli/test_skill_cli_parity.py), documents the two-name convention
(skills/ plural for package-data vs skill/ singular for the Python lib),
the planted-violation self-check, and the wheel exclusion defence for
maintainer-only .claude/skills/.

cli-layer.md § Multi-surface parity gains a paragraph naming the bundled
skill as the 6th parity surface, cross-linking to skill-parity.md and the
test_skill_cli_parity.py gate.

ORCHESTRATOR-ONLY commit per ralph-worker-claude-dir-perms memory —
workers cannot Write under .claude/ in worktrees.

Plan: plans/super/141-claude-skill-install.md US-009 / DEC-018, 019.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + README pointer)

NEW docs/skills.md catalog page describing the bundled SignalForge skill, the
install-skill subcommand, the two demo paths (zero-cred default + opt-in live
e2e), the parity gate, and the maintainer-only-skill exclusion. mkdocs.yml nav
gains 'Claude Code Skill' entry. docs/cli-ops.md gains the install-skill
subcommand entry with stderr shapes + exit codes. README Quick start gains a
one-sentence pointer after pip install.

clauditor badge intentionally NOT added here — US-008 owns that surface.

Plan: plans/super/141-claude-skill-install.md US-006 / DEC-021, 023.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors tests/cli/test_5_surface_parity_init_demo.py for the install-skill
subcommand. v0.1 canonical token: 'install-skill' (no flags). Pins the token
across argparse help, handler docstring, docs/cli-ops.md, plan document, and
the test docstring itself.

Orthogonal to test_skill_cli_parity.py (US-004): that scans the FULL CLI
surface against ONE skill body; this pins ONE subcommand across FIVE surfaces.

Plan: plans/super/141-claude-skill-install.md US-005 / DEC-024.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds clauditor-eval to [dependency-groups].dev (PyPI dist name; provides
the 'clauditor' CLI entry point — the upstream LLM-as-judge framework
SignalForge's own grading layer shares its methodology with). Grade
pending — clauditor-eval installs cleanly and 'uv run clauditor grade'
runs, but a meaningful score requires a maintainer-crafted EvalSpec
(SignalForge-specific assertions + grading criteria); the auto-scaffolded
'clauditor init' template is generic boilerplate that would grade
noise-against-noise. Refined assets/SKILL.eval.json to a
'pending-first-grade' shape that pins the current signalforge.__version__,
names the grader and regen command, and explains why the maintainer must
hand-tune an EvalSpec before the first real grade. README shields.io
badge surfaces the pending state ('clauditor: pending', lightgrey). New
'Self-grade' section in docs/skills.md documents the regen flow for the
maintainer's pre-release workflow per DEC-014.

VALIDATE_CMD green: 2745 passed, all four checks. wheel_smoke + cli_subprocess
gated markers also green.

Plan: plans/super/141-claude-skill-install.md US-008 / DEC-014.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four code-review passes surfaced 8 real findings; this commit addresses them.

CORRECTNESS
- src/signalforge/__init__.py: revert __version__ from 0.4.0.dev0 back to
  0.5.0.dev0 — the dev branch is the 0.5.0 development line; the previous
  value was a stale-rebase artifact (Review 1).
- src/signalforge/skill/__init__.py: extend symlink defence (DEC-005) to
  cover every install-tree ancestor (.claude/, .claude/skills/,
  .claude/skills/signalforge/) — not just SKILL.md itself. A symlinked
  ancestor dir would otherwise smuggle writes through copytree (Review 1).
- tests/skill/test_install.py: new test_install_skill_refuses_when_install_dir_ancestor_is_symlink
  pins the ancestor-symlink defence with a concrete attacker-elsewhere
  repro.

SKILL.md PROSE (drift between skill and live CLI surface)
- Remove the "--force" example (DEC-003 explicitly forbids the flag;
  re-running install-skill overwrites SKILL.md by default and preserves
  siblings).
- Frontmatter signalforge-version: "0.X.Y" → "0.5.0.dev0" (matches
  __version__; was a literal placeholder shipped to operators).
- Section 2: "no warehouse, no API keys, no dbt profile" was misleading
  — the drafter always calls Anthropic. Reword to "no dbt profile / no
  warehouse credentials of your own" + name the ANTHROPIC_API_KEY
  requirement.
- LLMCacheTooLargeError mis-labelled tier-3 → corrected to tier-2 (the
  cache-too-large check is a pre-LLM-call input-validation gate per
  cli-layer.md's four-tier taxonomy).

docs/skills.md DRIFT
- Canonical-commands list said "signalforge --version" — DEC-015's
  hardcoded list is init-demo / generate <model> --write /
  prune-existing <model> --schema <path> / install-skill (no --version
  variant; the parity gate scans for `version` subcommand separately
  via category 1).
- "signalforge --version is the first thing it runs" → "signalforge
  version (the subcommand)" — flag vs subcommand mismatch.

PARITY GATE EXTENSION (the gate would have caught the --force bug)
- tests/cli/test_skill_cli_parity.py: add fourth category that scans
  SKILL.md for `signalforge <subcommand> --<flag>` patterns and
  asserts each flag exists on the live subparser. Pinned with the
  same planted-violation philosophy (verified manually:
  reinstating --force fails the gate loud). Closes the "skill prose
  teaches a flag that doesn't exist" failure mode the original gate
  could not catch.

Validation:
- uv run ruff check . — clean
- uv run ruff format --check . — clean
- uv run pyright — 0 errors
- uv run pytest — 2747 passed (was 2745 baseline; +2 new tests)
- uv run pytest -m wheel_smoke --no-cov — 5/5
- uv run pytest -m cli_subprocess --no-cov — 8/8

Plan: plans/super/141-claude-skill-install.md US-010.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow wjduenow changed the title #141: SignalForge skill + install-skill (plan) #141: SignalForge skill + install-skill May 30, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow
wjduenow marked this pull request as ready for review May 30, 2026 18:26

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plans/super/141-claude-skill-install.md`:
- Around line 756-762: Remove the duplicate placeholder sections: delete the
second occurrences of the headings "## Refinement log" and "## Detailed
breakdown" that contain the "_Pending Phase 3._" and "_Pending Phase 4._" lines
(these are inconsistent with the document header "Phase: implemented" and with
the already-populated refinement log and detailed breakdown earlier in the
file); keep the populated sections (the earlier "Refinement log" and "Detailed
breakdown") and remove only the duplicate placeholder headers and their
placeholder text so the document reflects the implemented status.

In `@src/signalforge/cli/install_skill.py`:
- Around line 167-177: The pre-probe currently calls Path(raw_dest).expanduser()
/ _INSTALLED_SKILL_REL and uses .exists(), which follows symlinks; change the
probe so it checks the terminal path for a symlink first (use .is_symlink()) and
only calls .exists() when .is_symlink() is False, setting existed_before based
on either is_symlink() OR exists(); keep the existing try/except OSError around
the probe and assign to the same existed_before variable so the DEC-017 suffix
logic continues to work without dereferencing SKILL.md symlinks.

In `@src/signalforge/skill/__init__.py`:
- Around line 146-151: The handler that treats
FileNotFoundError/NotADirectoryError as “destination doesn’t exist yet” must
instead map cases where a non-directory ancestor exists to SkillDestUnsafeError;
update install_skill so after computing resolved_dest =
expanded_dest.resolve(strict=False) you iterate the ancestor chain (e.g. for p
in expanded_dest.parents or resolved_dest.parents) and if any ancestor.exists()
and not ancestor.is_dir() raise SkillDestUnsafeError with that ancestor/path
info, and keep the original fallback behavior only when no non-directory
ancestor is present; reference install_skill,
resolved_dest/expanded_dest.resolve, target_skill_dir.mkdir, and
SkillDestUnsafeError to locate and implement this change.

In `@src/signalforge/skills/signalforge/assets/SKILL.eval.json`:
- Line 3: The eval metadata in SKILL.eval.json still has "version": "0.4.0.dev0"
which should match the package's new version; update the "version" field in
SKILL.eval.json (the JSON key "version") to the package's current version (e.g.,
"0.5.0.dev0") so the eval metadata aligns with the released package version.

In `@src/signalforge/skills/signalforge/SKILL.md`:
- Line 16: The fenced code block that begins with the line "model.sql + manifest
+ project ctx" in SKILL.md is missing a language tag; update that fence to
include a language specifier (suggest "text") so the opening fence becomes
```text to satisfy MD040 (`fenced-code-language`) and prevent the lint warning
while preserving the block contents and formatting.

In `@tests/cli/test_skill_cli_parity.py`:
- Around line 246-253: The current _SKILL_FLAG_USAGE_RE only matches flags
immediately following the subcommand so forms like "signalforge generate <model>
--write" are missed; update the regex used by _SKILL_FLAG_USAGE_RE to allow
arbitrary (non-newline) tokens between the subcommand and the long flag while
still capturing the subcommand and the flag separately and preserving the
ASCII-only flag restriction—for example, change the pattern to match
"signalforge ([a-z][a-z0-9-]*)" then any chars up to the flag and capture
"(--[a-z][a-z0-9-]*)" (e.g. use a non-greedy [^\n]*? between the two capture
groups) so positional args are allowed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 784bb06c-62a2-4bd9-ad18-8f888df0017c

📥 Commits

Reviewing files that changed from the base of the PR and between d5a0031 and 12e6575.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .claude/rules/cli-layer.md
  • .claude/rules/skill-parity.md
  • README.md
  • docs/cli-ops.md
  • docs/skills.md
  • mkdocs.yml
  • plans/super/141-claude-skill-install.md
  • pyproject.toml
  • src/signalforge/__init__.py
  • src/signalforge/cli/__init__.py
  • src/signalforge/cli/_helpers.py
  • src/signalforge/cli/errors.py
  • src/signalforge/cli/install_skill.py
  • src/signalforge/skill/__init__.py
  • src/signalforge/skill/errors.py
  • src/signalforge/skills/signalforge/SKILL.md
  • src/signalforge/skills/signalforge/assets/SKILL.eval.json
  • tests/cli/test_5_surface_parity_install_skill.py
  • tests/cli/test_exit_codes.py
  • tests/cli/test_install_skill.py
  • tests/cli/test_skill_cli_parity.py
  • tests/cli/test_subprocess_smoke.py
  • tests/skill/test_install.py
  • tests/test_audit_completeness.py
  • tests/test_wheel_packaging.py

Comment thread plans/super/141-claude-skill-install.md Outdated
Comment thread src/signalforge/cli/install_skill.py
Comment thread src/signalforge/skill/__init__.py
Comment thread src/signalforge/skills/signalforge/assets/SKILL.eval.json Outdated
Comment thread src/signalforge/skills/signalforge/SKILL.md Outdated
Comment thread tests/cli/test_skill_cli_parity.py Outdated
codecov flagged 5 missed lines in the PR diff:
- src/signalforge/cli/install_skill.py:177-178 — except OSError around the
  existed_before probe (silently downgrades on probe failure; the install
  should still proceed)
- src/signalforge/skill/__init__.py:150 — resolve(strict=False) fallback
  for a dest that doesn't exist yet (the common fresh-project case)
- src/signalforge/skill/__init__.py:157 — non-ELOOP OSError raw re-raise
  (narrow ELOOP-only routing is load-bearing — a PermissionError must
  NOT be mis-attributed as a symlink cycle)
- src/signalforge/skill/errors.py:66 — SkillError.__str__'s no-footer
  branch (when neither remediation kwarg nor default_remediation is set)

Per the qg-pass-3-defer-defensive-tests-fails-codecov memory, codecov
holds patch coverage to project standard regardless of "is this a real
bug today" — defensive branches need test coverage even when they're
fallbacks.

Adds 4 tests:
- test_skill_error_str_omits_footer_when_remediation_is_none (errors.py:66)
- test_install_skill_propagates_non_eloop_oserror_unchanged (skill:157)
- test_install_skill_resolves_nonexistent_dest_via_strict_false_fallback (skill:150)
- test_install_skill_handles_oserror_in_existed_before_probe (cli:177-178)

Coverage: install_skill.py 95% → 100%, skill/__init__.py 95% → 100%,
skill/errors.py 95% → 100%. Full pytest: 2751 passed (was 2747).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

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 ships the SignalForge Claude Code skill requested by #141, bundles it in the wheel, and adds a signalforge install-skill path that mirrors the existing init-demo library/CLI/error-handling pattern.

Changes:

  • Adds bundled skill package data plus wheel smoke gates and docs.
  • Adds public signalforge.skill.install_skill() with typed errors and a new install-skill CLI subcommand.
  • Adds parity/audit tests to keep the skill, CLI, docs, and packaging surfaces aligned.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
uv.lock Locks new dev dependency for clauditor self-grading.
pyproject.toml Adds skill package data to wheel include and clauditor dev dependency.
src/signalforge/__init__.py Bumps package version to 0.5.0.dev0.
src/signalforge/skill/__init__.py Implements public skill installer.
src/signalforge/skill/errors.py Adds typed skill-install error hierarchy.
src/signalforge/cli/install_skill.py Adds CLI handler and parser for install-skill.
src/signalforge/cli/errors.py Adds CLI wrapper errors for install-skill failures.
src/signalforge/cli/_helpers.py Registers new skill errors in exit-code mapping.
src/signalforge/cli/__init__.py Registers the new subcommand.
src/signalforge/skills/signalforge/SKILL.md Adds the shipped Claude Code skill body.
src/signalforge/skills/signalforge/assets/SKILL.eval.json Adds placeholder self-grade sidecar.
tests/skill/test_install.py Adds library installer tests.
tests/cli/test_install_skill.py Adds CLI behavior and exit-code tests.
tests/cli/test_subprocess_smoke.py Adds subprocess help smoke for install-skill.
tests/cli/test_skill_cli_parity.py Adds SKILL ↔ CLI parity gate.
tests/cli/test_5_surface_parity_install_skill.py Adds install-skill multi-surface parity test.
tests/cli/test_exit_codes.py Extends synthetic constructor coverage for new CLI errors.
tests/test_audit_completeness.py Extends error-module audit scan for signalforge.skill.
tests/test_wheel_packaging.py Adds positive/negative wheel assertions for bundled skills.
README.md Adds skill install pointer and self-grade badge.
docs/skills.md Adds skill catalog and operating model docs.
docs/cli-ops.md Documents install-skill behavior and exits.
mkdocs.yml Adds skill docs to navigation.
plans/super/141-claude-skill-install.md Adds implementation plan and decisions.
.claude/rules/skill-parity.md Adds contributor rule for skill parity.
.claude/rules/cli-layer.md Adds bundled skill as a sixth CLI parity surface.

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

Comment thread src/signalforge/skill/__init__.py Outdated
Comment thread src/signalforge/skills/signalforge/assets/SKILL.eval.json Outdated
Comment thread tests/cli/test_skill_cli_parity.py Outdated
Comment thread src/signalforge/skills/signalforge/SKILL.md Outdated
Comment thread src/signalforge/skills/signalforge/SKILL.md Outdated
Comment thread src/signalforge/skills/signalforge/SKILL.md
Comment thread tests/cli/test_skill_cli_parity.py Outdated

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
tests/cli/test_install_skill.py (1)

432-438: ⚡ Quick win

Assert the install still happened after the probe failure.

This regression test proves the exists() OSError no longer aborts the command, but it would still pass if the handler returned 0 without actually calling install_skill. Add a direct assertion that tmp_path / _INSTALLED_REL exists (and ideally that stdout still contains the normal install message) to pin the intended behavior end to end.

Suggested tightening
     ret = main(["install-skill", str(tmp_path)])
     out, err = _capture(capsys)
     # Probe-OSError silently downgrades → install proceeds → exit 0.
     assert ret == 0, f"expected install to proceed; got {ret}\nstdout: {out}\nstderr: {err}"
+    assert (tmp_path / _INSTALLED_REL).is_file()
+    assert out.startswith("Installed SignalForge skill to ")
     # Probe failed → existed_before stays False → no `(replaced existing SKILL.md)` suffix.
     assert "(replaced existing SKILL.md)" not in out
     assert "Traceback" not in err
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/cli/test_install_skill.py` around lines 432 - 438, The test currently
only asserts exit code and absence of error, but doesn't verify the install
actually created files; update the test around the main(["install-skill",
str(tmp_path)]) invocation to assert the installed artifact exists by checking
(tmp_path / _INSTALLED_REL).exists() and also assert that stdout contains the
normal install success message (e.g. the message produced by install_skill) to
ensure install_skill was invoked; reference main, install-skill, tmp_path,
_INSTALLED_REL and install_skill when adding these assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/cli/test_install_skill.py`:
- Around line 432-438: The test currently only asserts exit code and absence of
error, but doesn't verify the install actually created files; update the test
around the main(["install-skill", str(tmp_path)]) invocation to assert the
installed artifact exists by checking (tmp_path / _INSTALLED_REL).exists() and
also assert that stdout contains the normal install success message (e.g. the
message produced by install_skill) to ensure install_skill was invoked;
reference main, install-skill, tmp_path, _INSTALLED_REL and install_skill when
adding these assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d0741cc-2826-47f1-ab92-4e069c558a1f

📥 Commits

Reviewing files that changed from the base of the PR and between 12e6575 and f8fb531.

📒 Files selected for processing (2)
  • tests/cli/test_install_skill.py
  • tests/skill/test_install.py

Trivial:
- assets/SKILL.eval.json: version 0.4.0.dev0 → 0.5.0.dev0 (matches the
  __version__ revert in the previous QG commit; flagged by both reviewers).
- SKILL.md fenced ASCII pipeline diagram: add `text` language tag (MD040).
- plans/super/141: drop the leftover duplicate "_Pending Phase 3/4._"
  placeholders at the end of the doc.

Substantive — symlink defence broadened to EVERY bundled path:
- src/signalforge/skill/__init__.py: the symlink defence used to cover
  only `<dest>/.claude/skills/signalforge/SKILL.md`. A symlinked
  `assets/SKILL.eval.json` (or symlinked `assets/` directory) would
  smuggle writes through copytree. Now enumerates every relative path
  under the bundled source tree via `rglob` and refuses to overwrite any
  of them through a symlink. Pinned by
  `test_install_skill_refuses_when_assets_dir_is_symlink`.
- Also wrap `mkdir(parents=True)` in try/except NotADirectoryError →
  raise SkillDestUnsafeError so a non-dir component along the install
  chain (e.g. `<dest>/.claude` is a regular file) yields a typed
  remediation-bearing message instead of a raw OSError. Pinned by
  `test_install_skill_wraps_notadirectoryerror_from_mkdir_chain`.

Substantive — CLI existed_before probe:
- src/signalforge/cli/install_skill.py: probe was `.exists()` which
  follows symlinks AND returns False for broken symlinks. The DEC-017
  contract says "True for files and symlinks (both shapes are replaced
  from the operator's POV)" — a broken symlink is a third shape that
  the probe silently downgrades. Probe now OR's `.is_symlink()` to
  catch the broken-symlink case (semantics stay honest even though
  the lib seam then raises SkillDestUnsafeError on the same path).

Substantive — SKILL.md ADC contradiction:
- The frontmatter `compatibility:` field and Section 2 body claimed
  "zero-credential demo (no warehouse needed)" while simultaneously
  describing the demo as sampling a public BigQuery dataset under ADC
  (which requires `gcloud auth application-default login` +
  GOOGLE_CLOUD_PROJECT). Both surfaces rewritten to honestly describe
  the demo's real posture: removes the dbt-project setup cost, but
  needs ANTHROPIC_API_KEY + ADC + GOOGLE_CLOUD_PROJECT. Section 2 also
  surfaces `signalforge lint --model <name>` as the truly-offline
  fallback (manifest-only, no LLM, no warehouse).

Substantive — dbt parse invocation:
- SKILL.md Section 1 suggested running `dbt parse` but the
  `allowed-tools` frontmatter does NOT include `Bash(dbt *)`. Reworded
  to "ask the user to run dbt parse themselves" rather than implying
  the skill runs it — preserves the narrow tool grant.

Substantive — parity gate (`test_skill_cli_parity.py`):
- Broaden `_SKILL_FLAG_USAGE_RE` to match `signalforge <subcommand>
  [<positional> ...] --<flag>` so canonical shapes like `generate
  <model> --write` and `prune-existing <model> --schema <path>` are no
  longer skipped (CodeRabbit + Copilot). Constrain to same-line
  `[ \t]` (not `\s`) so the match cannot span newlines — without
  this, prose like "signalforge installed (pip install ...)" plus
  "signalforge lint --model" two paragraphs later yields a spurious
  `installed --model` capture (caught during validation).
- Unknown-subcommand branch now FAILS instead of skipping (Copilot).
  A typo like `signalforge instal-skill --force` would previously
  skip silently; now it surfaces in the assertion message.
- Verified via planted-violation: `signalforge generate <model>
  --xyzbogus` injection trips the gate; restoring SKILL.md returns
  the gate to green.

Validation:
- uv run ruff check . — clean
- uv run ruff format --check . — clean
- uv run pyright — 0 errors
- uv run pytest — 2753 passed (was 2751; +2 new tests)
- uv run pytest -m wheel_smoke --no-cov — 5/5
- Patch coverage: install_skill.py 100%, skill/__init__.py 100%,
  skill/errors.py 100% — codecov-clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary (CodeRabbit + Copilot, round 1)

All 13 unresolved threads addressed in commit bd8fd31. No deferrals — every comment is either fixed inline or documented as a false positive below.

Fixed (13 items)

# File Line Issue Reviewer Fix
1 assets/SKILL.eval.json 3 Version stale at 0.4.0.dev0 (PR ships 0.5.0.dev0) CodeRabbit + Copilot Bumped to 0.5.0.dev0
2 SKILL.md 16 Fenced code block missing language tag (MD040) CodeRabbit Added text tag
3 plans/super/141…md 762 Duplicate _Pending Phase 3/4._ headers CodeRabbit Removed the leftover stubs
4 src/signalforge/skill/__init__.py 196 Symlink defence only covered SKILL.md — assets/SKILL.eval.json and assets/ dir could still be smuggled through Copilot Now enumerates every bundled path via rglob and checks each is_symlink()
5 src/signalforge/skill/__init__.py 151 mkdir(parents=True) raises raw NotADirectoryError when a non-dir blocks the install chain CodeRabbit Wrapped to SkillDestUnsafeError with typed remediation
6 src/signalforge/cli/install_skill.py 177 .exists() follows symlinks and returns False for broken symlinks — silently skips the (replaced existing SKILL.md) suffix CodeRabbit Probe now ORs .is_symlink() to catch broken symlinks too
7 tests/cli/test_skill_cli_parity.py 253 Regex missed signalforge generate <model> --write shape — stale flags could slip past gate CodeRabbit + Copilot Broadened to (?:[ \t]+\S+)*? (same-line only to avoid spurious cross-paragraph captures)
8 tests/cli/test_skill_cli_parity.py 309 Unknown-subcommand branch silently skipped — typos like instal-skill slipped through Copilot Branch now fails loud with subcommand-named assertion
9 SKILL.md 4 Frontmatter compatibility: claimed "zero-credential demo, no warehouse needed" while simultaneously requiring ADC Copilot Rewritten to honestly describe the demo's posture (needs ANTHROPIC_API_KEY + ADC + GOOGLE_CLOUD_PROJECT; the truly offline path is signalforge lint)
10 SKILL.md 74 Section 2 body had the same contradiction Copilot Rewritten alongside #9
11 SKILL.md 7 Body suggested running dbt parse but allowed-tools doesn't grant Bash(dbt *) Copilot Reworded to "ask the user to run dbt parse" — preserves the narrow tool grant

Verification

  • New planted-violation test for the broader regex: injecting signalforge generate <model> --xyzbogus fails the gate; restoring SKILL.md returns it to green.
  • New tests pinning the QG-extended symlink defence + the NotADirectoryError wrap: test_install_skill_refuses_when_assets_dir_is_symlink, test_install_skill_wraps_notadirectoryerror_from_mkdir_chain.
  • Full pytest: 2753 passed (was 2751; +2 new tests).
  • Patch coverage: install_skill.py 100%, skill/__init__.py 100%, skill/errors.py 100%.
  • wheel_smoke: 5/5; cli_subprocess: 8/8; ruff/format/pyright all clean.

False positives

None — every finding was a real bug or a documented contradiction.

🤖 Resolved with Claude Code

@wjduenow
wjduenow merged commit 709f147 into dev May 30, 2026
6 checks passed
@wjduenow
wjduenow deleted the feature/141-claude-skill-install branch May 30, 2026 21:37
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.

3 participants