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
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,17 +233,18 @@ generic_automation_mcp/
│ ├── _terminal.py # terminal_guard() TTY restore
│ ├── _terminal_table.py # Re-export shim from core/_terminal_table
│ ├── _cook.py # cook: ephemeral skill session launcher
│ ├── _doctor.py # 12 project setup checks
│ ├── _doctor.py # 16 project setup checks
│ ├── _hooks.py # PreToolUse hook registration helpers
│ ├── _init_helpers.py
│ ├── _install_info.py # InstallInfo, InstallType, detect_install(), comparison_branch(), dismissal_window(), upgrade_command()
│ ├── _marketplace.py # Plugin install/upgrade
│ ├── _mcp_names.py # MCP prefix detection
│ ├── _onboarding.py # First-run detection + guided menu
│ ├── _prompts.py # Orchestrator prompt builder
│ ├── _source_drift.py # Source-drift boot gate: install-type detection, reference-SHA resolution, dismissal-aware CLI gate
│ ├── _stale_check.py # Version comparison, hook-drift prompt
│ ├── _update.py # run_update_command(): first-class upgrade path for `autoskillit update`
│ ├── _update_checks.py # Unified startup update check: version/hook/source-drift signals, branch-aware dismissal
│ ├── _workspace.py # Workspace clean helpers
│ └── app.py # CLI entry: serve, init, config, skills, recipes, doctor, etc.
│ └── app.py # CLI entry: serve, init, config, skills, recipes, doctor, update, etc.
├── hooks/ # Claude Code PreToolUse/PostToolUse/SessionStart scripts
│ ├── __init__.py
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ two-tier orchestrator. The bundled recipes implement issue → plan → worktree
## Quick reference

- [cli.md](cli.md) — every `autoskillit` command
- [update-checks.md](update-checks.md) — update checks, dismissal windows, `autoskillit update`
- [faq.md](faq.md) — common questions
- [glossary.md](glossary.md) — canonical terms

Expand Down
18 changes: 15 additions & 3 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ projects need.

autoskillit doctor

Doctor runs 15 checks (13 numbered + 2 lettered sub-checks `4b` and `7b`),
Doctor runs 17 checks (15 numbered + 2 lettered sub-checks `4b` and `7b`),
enumerated by `run_doctor` in `src/autoskillit/cli/_doctor.py`:

| # | Check | What it verifies |
Expand All @@ -86,13 +86,25 @@ enumerated by `run_doctor` in `src/autoskillit/cli/_doctor.py`:
| 10 | Secret scanning hook | `gitleaks` (or equivalent) is installed as a pre-commit hook |
| 11 | Editable install source exists | An editable install still points at a real source directory |
| 12 | No stale entry points | No leftover `autoskillit` scripts outside `~/.local/bin` |
| 13 | Source version drift | Installed commit SHA matches the last-known HEAD of the installed branch |
| 14 | Quota cache schema | `autoskillit_quota_cache.json` schema version is current |
| 13 | Source version drift | Installed commit SHA vs. branch HEAD (network, with cache fallback) |
| 14 | Quota cache schema | `~/.claude/autoskillit_quota_cache.json` schema version is current |
| 15 | Claude process state | Reports D-state and CPU breakdown of running `claude` processes via `ps` |
| 16 | Install classification | `direct_url.json` classifies the install type and requested revision |
| 17 | Update dismissal state | Active update-prompt dismissal window and conditions, if any |

See **[Hooks](safety/hooks.md)** for what each PreToolUse / PostToolUse /
SessionStart hook actually enforces.

## Updating

AutoSkillit checks for updates on every interactive invocation and shows a single
consolidated `[Y/n]` prompt when updates are available. For details on how update
checks work, dismissal windows, and escape hatches, see **[Update Checks](update-checks.md)**.

To update immediately without waiting for the prompt:

autoskillit update

## Troubleshooting

### "autoskillit: command not found"
Expand Down
65 changes: 65 additions & 0 deletions docs/update-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Updating AutoSkillit

## How update checks work

On every interactive CLI invocation (excluding headless/MCP sessions and CI),
AutoSkillit checks for available updates and shows a single `[Y/n]` prompt if
any of the following conditions fire:

- **binary** — a newer release is available on your install's branch
- **hooks** — new or changed hook entries have been added since last install
- **branch drift** — the installed commit SHA lags the HEAD of your tracked branch

All three conditions are consolidated into a single prompt listing each reason.
Answering `Y` runs the appropriate upgrade command followed by `autoskillit install`.

## Branch-aware dismissal windows

Dismissal windows vary by install type to balance convenience and safety:

| Install | Window |
|---------|--------|
| stable / main / release-tag | 7 days |
| integration / local-editable | 12 hours |

The window is determined at check time from the current `direct_url.json` —
not from what was stored when you dismissed.

Dismissal expires on two axes:

1. **Time** — the window elapses.
2. **Version delta** — the running version advances past the dismissed version.

## The `autoskillit update` command

To upgrade immediately without waiting for a prompt:

autoskillit update

This runs the install-type-aware upgrade command, then `autoskillit install`,
then verifies that the version advanced. On success it clears any active
dismissal state so the next check starts fresh.

For unknown install types (e.g. installed from PyPI without a VCS reference),
`autoskillit update` exits with code 2 and prints a reinstallation hint.

## Escape hatches

Set either env var to silence all update checks for a single invocation:

AUTOSKILLIT_SKIP_STALE_CHECK=1 autoskillit <command>
AUTOSKILLIT_SKIP_SOURCE_DRIFT_CHECK=1 autoskillit <command>

Both are automatically injected by the update logic itself so that subprocesses
launched during an update do not re-enter the check.

## Install detection

Update checks read `direct_url.json` from the installed package metadata
(populated by `uv` or `pip` at install time). The `~/.autoskillit/dev` marker
file is no longer consulted — install classification is derived entirely from
`direct_url.json`.

Use `autoskillit doctor` to inspect the current classification:

install_classification: install_type=git-vcs, requested_revision=stable, commit_id=abc12345
2 changes: 2 additions & 0 deletions src/autoskillit/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
serve,
skills_app,
skills_list,
update,
upgrade,
workspace_app,
workspace_clean,
Expand Down Expand Up @@ -81,6 +82,7 @@
"serve",
"skills_app",
"skills_list",
"update",
"upgrade",
"workspace_app",
"workspace_clean",
Expand Down
89 changes: 76 additions & 13 deletions src/autoskillit/cli/_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,21 +343,18 @@ def _check_config_layers_for_secrets(


def _check_source_version_drift(home: Path | None = None) -> DoctorResult:
"""Cache-only source-drift check.
"""Network source-drift check.

Compares the installed commit SHA against the last-known HEAD of the branch
the binary was installed from. Uses the disk cache written by previous
online invocations — **never makes a network request**.
Compares the installed commit SHA against the current HEAD of the branch
the binary was installed from. Uses a network request to get the latest
SHA (with disk-cache TTL fallback).
"""
check_name = "source_version_drift"
_home = home or Path.home()

try:
from autoskillit.cli._source_drift import (
InstallType,
detect_install,
resolve_reference_sha,
)
from autoskillit.cli._install_info import InstallType, detect_install
from autoskillit.cli._update_checks import resolve_reference_sha

info = detect_install()

Expand All @@ -373,14 +370,14 @@ def _check_source_version_drift(home: Path | None = None) -> DoctorResult:
"Not a source-tracked install — drift check not applicable",
)

# GIT_VCS: resolve SHA from disk cache only (network=False)
ref_sha = resolve_reference_sha(info, _home, network=False)
# GIT_VCS: resolve SHA via network (with disk-cache fallback)
ref_sha = resolve_reference_sha(info, _home, network=True)

if ref_sha is None:
return DoctorResult(
Severity.OK,
check_name,
"Source drift cache is emptyrun a command online to populate the check",
"Source drift reference SHA unavailablecheck network connectivity",
)

if info.commit_id == ref_sha:
Expand All @@ -402,6 +399,66 @@ def _check_source_version_drift(home: Path | None = None) -> DoctorResult:
)


def _check_install_classification() -> DoctorResult:
"""Classify the current autoskillit install type via direct_url.json."""
check_name = "install_classification"
try:
from autoskillit.cli._install_info import InstallType, detect_install

info = detect_install()
if info.install_type == InstallType.UNKNOWN:
return DoctorResult(
Severity.WARNING,
check_name,
"install type could not be detected from direct_url.json",
)
commit_short = (info.commit_id or "")[:8]
return DoctorResult(
Severity.OK,
check_name,
f"install_type={info.install_type}, "
f"requested_revision={info.requested_revision}, "
f"commit_id={commit_short}",
)
except Exception:
_log.debug("Install classification check failed", exc_info=True)
return DoctorResult(
Severity.OK, check_name, "Install classification check skipped (unexpected error)"
)


def _check_update_dismissal_state(home: Path | None = None) -> DoctorResult:
"""Report the current update-prompt dismissal state."""
check_name = "update_dismissal_state"
_home = home or Path.home()
try:
from autoskillit.cli._install_info import detect_install, dismissal_window
from autoskillit.cli._update_checks import _read_dismiss_state

state = _read_dismiss_state(_home)
entry = state.get("update_prompt")
if not isinstance(entry, dict) or "dismissed_at" not in entry:
return DoctorResult(Severity.OK, check_name, "No active dismissal")

from datetime import datetime

info = detect_install()
window = dismissal_window(info)
dismissed_at = datetime.fromisoformat(str(entry["dismissed_at"]))
expiry = (dismissed_at + window).strftime("%Y-%m-%d")
conditions = entry.get("conditions", [])
return DoctorResult(
Severity.OK,
check_name,
f"update_prompt dismissed until {expiry}; conditions={conditions}",
)
except Exception:
_log.debug("Update dismissal state check failed", exc_info=True)
return DoctorResult(
Severity.OK, check_name, "Update dismissal state check skipped (unexpected error)"
)


def _check_quota_cache_schema(cache_path: Path | None = None) -> DoctorResult:
"""Check the quota cache file for schema version drift."""
check_name = "quota_cache_schema"
Expand Down Expand Up @@ -688,7 +745,7 @@ def run_doctor(*, output_json: bool = False) -> None:
# Check 12: No stale autoskillit entry points outside ~/.local/bin
results.append(_check_stale_entry_points())

# Check 13: Source version drift (cache-only, never network)
# Check 13: Source version drift (network, with disk-cache TTL fallback)
results.append(_check_source_version_drift())

# Check 14: Quota cache schema version
Expand All @@ -697,6 +754,12 @@ def run_doctor(*, output_json: bool = False) -> None:
# Check 15: claude process state breakdown
results.append(_check_claude_process_state_breakdown())

# Check 16: Install classification from direct_url.json
results.append(_check_install_classification())

# Check 17: Update-prompt dismissal state
results.append(_check_update_dismissal_state())

# Output
if output_json:
print(
Expand Down
Loading
Loading