Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -232,5 +232,4 @@ prd.md
.specleft/specs/overview.md
.licenses/policy.yml
bandit-report.json
docs/SKILL.md
PLAN.md
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ Guaranteed invariants:
- All validation errors are explicit and actionable
- Missing behaviour is skipped, never failed
- CLI exit codes are meaningful and stable
- Skill instructions are integrity-verifiable via `specleft skill verify`
- Skill commands are constrained to simple `specleft ...` invocations (no shell metacharacters)

SpecLeft commands are safe to:
- run repeatedly
Expand Down
119 changes: 119 additions & 0 deletions docs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# SpecLeft CLI Reference

## Workflow
1. specleft next --format json
2. Implement test logic
3. specleft features validate --format json
4. specleft skill verify --format json
5. pytest
6. Repeat

## Safety
- Always `--dry-run` before writing files
- Never use `--force` unless explicitly requested
- Exit codes: 0 = success, 1 = error, 2 = cancelled
- Commands are deterministic and safe to retry

---

## Features

### Validate specs
`specleft features validate --format json [--dir PATH] [--strict]`
Validate before generating tests. `--strict` treats warnings as errors.

### List features
`specleft features list --format json [--dir PATH]`

### Show stats
`specleft features stats --format json [--dir PATH] [--tests-dir PATH]`

### Add a feature
`specleft features add --format json --id FEATURE_ID --title "Title" [--priority PRIORITY] [--description TEXT] [--dir PATH] [--dry-run]`
Creates `<features-dir>/feature-id.md`. Never overwrites existing files.
Use `--interactive` for guided prompts (TTY only).

### Add a scenario
`specleft features add-scenario --format json --feature FEATURE_ID --title "Title" [--id SCENARIO_ID] [--step "Given ..."] [--step "When ..."] [--step "Then ..."] [--priority PRIORITY] [--tags "tag1,tag2"] [--dir PATH] [--tests-dir PATH] [--dry-run] [--add-test MODE] [--preview-test]`
Appends to feature file. `--add-test` generates a test file.
`--preview-test` shows test content without writing. Use `--interactive`
for guided prompts (TTY only).

## Status and Planning

### Show status
`specleft status --format json [--dir PATH] [--feature ID] [--story ID] [--unimplemented] [--implemented]`

### Next scenario to implement
`specleft next --format json [--dir PATH] [--limit N] [--priority PRIORITY] [--feature ID] [--story ID]`

### Coverage metrics
`specleft coverage --format json [--dir PATH] [--threshold N] [--output PATH]`
`--threshold N` exits non-zero if coverage drops below `N%`.

## Test Generation

### Generate skeleton tests
`specleft test skeleton --format json [-f FEATURES_DIR] [-o OUTPUT_DIR] [--dry-run] [--force] [--single-file] [--skip-preview]`
Always run `--dry-run` first. Never overwrite without `--force`.

### Generate stub tests
`specleft test stub --format json [-f FEATURES_DIR] [-o OUTPUT_DIR] [--dry-run] [--force] [--single-file] [--skip-preview]`
Minimal test scaffolding with the same overwrite safety rules.

### Generate test report
`specleft test report --format json [-r RESULTS_FILE] [-o OUTPUT_PATH] [--open-browser]`
Builds an HTML report from `.specleft/results/`.

## Planning

### Generate specs from PRD
`specleft plan --format json [--from PATH] [--dry-run] [--analyze] [--template PATH]`
`--analyze` inspects PRD structure without writing files.
`--template` uses a YAML section-matching template.

## Contract

### Show contract
`specleft contract --format json`

### Verify contract
`specleft contract test --format json [--verbose]`
Run to verify deterministic and safe command guarantees.

## Skill Command Group

### Show skill commands
`specleft skill --help`
Displays skill lifecycle subcommands (`verify`, `update`).

### Verify skill integrity
`specleft skill verify --format json`
Returns `pass`, `modified`, or `outdated` integrity status.

### Update skill files
`specleft skill update --format json`
Regenerates `.specleft/SKILL.md` and `.specleft/SKILL.md.sha256`.

### Verify within doctor checks
`specleft doctor --verify-skill --format json`
Adds skill integrity status to standard environment diagnostics.

## Enforcement

### Enforce policy
`specleft enforce [POLICY_FILE] --format json [--dir PATH] [--tests PATH] [--ignore-feature-id ID]`
Default policy: `.specleft/policies/policy.yml`.
Exit codes: 0 = satisfied, 1 = violated, 2 = license issue.

## License

### License status
`specleft license status [--file PATH]`
Show license status and validated policy metadata.
Default: `.specleft/policies/policy.yml`.

## Guide

### Show workflow guide
`specleft guide --format json`
41 changes: 41 additions & 0 deletions features/feature-skill-integrity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Feature: Skill File Integrity Verification

## Scenarios

### Scenario: Skill command group is discoverable
priority: high

- Given the SpecLeft CLI is available
- When `specleft skill --help` is invoked
- Then the output lists `verify` and `update` subcommands

### Scenario: Verify reports pass after init
priority: high

- Given `specleft init` has generated `.specleft/SKILL.md`
- And a matching `.specleft/SKILL.md.sha256` exists
- When `specleft skill verify --format json` is invoked
- Then the integrity status is `pass`

### Scenario: Verify reports modified on hash mismatch
priority: high

- Given `.specleft/SKILL.md` content is modified after generation
- And `.specleft/SKILL.md.sha256` still has the previous hash
- When `specleft skill verify --format json` is invoked
- Then the integrity status is `modified`

### Scenario: Verify reports outdated for non-canonical but checksum-valid content
priority: medium

- Given `.specleft/SKILL.md` matches its checksum file
- But the content differs from the current packaged template
- When `specleft skill verify --format json` is invoked
- Then the integrity status is `outdated`

### Scenario: Skill update repairs modified integrity state
priority: high

- Given `.specleft/SKILL.md` and `.specleft/SKILL.md.sha256` are inconsistent
- When `specleft skill update --format json` is invoked
- Then `specleft skill verify --format json` reports integrity `pass`
2 changes: 2 additions & 0 deletions src/specleft/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
license_group,
next_command,
plan,
skill_group,
status,
test,
)
Expand Down Expand Up @@ -50,6 +51,7 @@ def cli() -> None:
cli.add_command(contract)
cli.add_command(enforce)
cli.add_command(license_group)
cli.add_command(skill_group)
cli.add_command(guide)


Expand Down
2 changes: 2 additions & 0 deletions src/specleft/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from specleft.commands.license import license_group
from specleft.commands.next import next_command
from specleft.commands.plan import plan
from specleft.commands.skill import skill_group
from specleft.commands.status import status
from specleft.commands.test import test

Expand All @@ -29,6 +30,7 @@
"license_group",
"next_command",
"plan",
"skill_group",
"status",
"test",
]
2 changes: 1 addition & 1 deletion src/specleft/commands/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
from specleft.version import SPECLEFT_VERSION

CLI_VERSION = SPECLEFT_VERSION
CONTRACT_VERSION = "1.0"
CONTRACT_VERSION = "1.1"
CONTRACT_DOC_PATH = "docs/agent-contract.md"
4 changes: 4 additions & 0 deletions src/specleft/commands/contracts/payloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ def build_contract_payload() -> dict[str, object]:
"cancelled": 2,
},
},
"skill_security": {
"skill_file_integrity_check": True,
"skill_file_commands_are_simple": True,
},
},
"docs": {
"agent_contract": CONTRACT_DOC_PATH,
Expand Down
13 changes: 13 additions & 0 deletions src/specleft/commands/contracts/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def print_contract_table(payload: Mapping[str, object]) -> None:
execution = cast(dict[str, Any], guarantees.get("execution", {}))
determinism = cast(dict[str, Any], guarantees.get("determinism", {}))
cli_api = cast(dict[str, Any], guarantees.get("cli_api", {}))
skill_security = cast(dict[str, Any], guarantees.get("skill_security", {}))
click.echo("SpecLeft Agent Contract")
click.echo("─" * 40)
click.echo(f"Contract version: {payload.get('contract_version')}")
Expand Down Expand Up @@ -84,6 +85,18 @@ def print_contract_table(payload: Mapping[str, object]) -> None:
)
click.echo(" - Stable exit codes: 0=success, 1=error, 2=cancel")
click.echo("")
click.echo("Skill Security:")
click.echo(
" - Skill file integrity is verifiable"
if skill_security.get("skill_file_integrity_check")
else " - Skill integrity guarantee missing"
)
click.echo(
" - Skill commands are simple invocations (no shell metacharacters)"
if skill_security.get("skill_file_commands_are_simple")
else " - Skill command simplicity guarantee missing"
)
click.echo("")
click.echo(f"For full details, see: {CONTRACT_DOC_PATH}")
click.echo("─" * 40)

Expand Down
66 changes: 56 additions & 10 deletions src/specleft/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@

from specleft.commands.constants import CLI_VERSION
from specleft.utils.messaging import print_support_footer
from specleft.utils.skill_integrity import (
INTEGRITY_MODIFIED,
INTEGRITY_OUTDATED,
verify_skill_integrity,
)
from specleft.utils.specs_dir import resolve_specs_dir


Expand Down Expand Up @@ -49,7 +54,7 @@ def _load_dependency_names() -> list[str]:
return parsed or dependencies


def _build_doctor_checks() -> dict[str, Any]:
def _build_doctor_checks(*, verify_skill: bool) -> dict[str, Any]:
import importlib.metadata as metadata

cli_check = {"status": "pass", "version": CLI_VERSION}
Expand Down Expand Up @@ -137,15 +142,28 @@ def _build_doctor_checks() -> dict[str, Any]:
"tests_writable": tests_writable,
}

checks: dict[str, Any] = {
"cli_available": cli_check,
"pytest_plugin": plugin_check,
"python_version": python_check,
"dependencies": dependency_check,
"directories": directory_check,
}
if verify_skill:
integrity_payload = verify_skill_integrity().to_payload()
integrity_status = str(integrity_payload.get("integrity"))
checks["skill_file_integrity"] = {
"status": (
"fail"
if integrity_status == INTEGRITY_MODIFIED
else ("warn" if integrity_status == INTEGRITY_OUTDATED else "pass")
),
**integrity_payload,
}

return {
"version": CLI_VERSION,
"checks": {
"cli_available": cli_check,
"pytest_plugin": plugin_check,
"python_version": python_check,
"dependencies": dependency_check,
"directories": directory_check,
},
"checks": checks,
}


Expand Down Expand Up @@ -182,6 +200,16 @@ def _build_doctor_output(checks: dict[str, Any]) -> dict[str, Any]:
errors.append("Feature/test directory access issue")
suggestions.append("Check directory permissions")

skill_check = checks_map.get("skill_file_integrity")
if isinstance(skill_check, dict):
integrity = skill_check.get("integrity")
if integrity == INTEGRITY_MODIFIED:
healthy = False
errors.append("Skill file integrity verification failed")
suggestions.append("Run: specleft skill update")
elif integrity == INTEGRITY_OUTDATED:
suggestions.append("Skill file is outdated. Run: specleft skill update")

output = {
"healthy": healthy,
"version": checks.get("version"),
Expand Down Expand Up @@ -234,6 +262,19 @@ def _print_doctor_table(checks: dict[str, Any], *, verbose: bool) -> None:
click.echo(f"{features_marker} Can read feature directory ({features_dir}/)")
click.echo(f"{tests_marker} Can write to test directory (tests/)")

skill_check = checks_map.get("skill_file_integrity")
if isinstance(skill_check, dict):
skill_marker = (
"✓"
if skill_check.get("status") == "pass"
else ("⚠" if skill_check.get("status") == "warn" else "✗")
)
click.echo(
f"{skill_marker} Skill file integrity ({skill_check.get('integrity')})"
)
if skill_check.get("message"):
click.echo(f" {skill_check.get('message')}")

if verbose and plugin_check.get("error"):
click.echo(f"pytest plugin error: {plugin_check.get('error')}")

Expand Down Expand Up @@ -262,9 +303,14 @@ def _print_doctor_table(checks: dict[str, Any], *, verbose: bool) -> None:
help="Output format: 'table' or 'json'.",
)
@click.option("--verbose", is_flag=True, help="Show detailed diagnostic information.")
def doctor(format_type: str, verbose: bool) -> None:
@click.option(
"--verify-skill",
is_flag=True,
help="Verify .specleft/SKILL.md checksum and template freshness.",
)
def doctor(format_type: str, verbose: bool, verify_skill: bool) -> None:
"""Verify SpecLeft installation and environment."""
checks = _build_doctor_checks()
checks = _build_doctor_checks(verify_skill=verify_skill)
output = _build_doctor_output(checks)

if format_type == "json":
Expand Down
Loading
Loading