Implementation Plan: CLI Update Prompts — Single Source of Truth + Dismissal UX#768
Conversation
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested
13 actionable findings (2 critical, 11 warning). See inline comments.
| logger = get_logger(__name__) | ||
|
|
||
| _DISMISS_FILE = "update_check.json" | ||
| _STABLE_DISMISS_WINDOW = timedelta(days=7) |
There was a problem hiding this comment.
[warning] slop: _STABLE_DISMISS_WINDOW and _DEV_DISMISS_WINDOW are module-level constants that duplicate the policy already encoded in dismissal_window() in _install_info.py. run_update_checks() calls dismissal_window(info) directly rather than these constants, making them dead values referenced only by a docstring-mirror test. Remove and derive from dismissal_window() instead.
There was a problem hiding this comment.
Investigated — this is intentional. _STABLE_DISMISS_WINDOW and _DEV_DISMISS_WINDOW are imported and actively used in tests/arch/test_subpackage_isolation.py:1215,1232–1233 in test_singleton_exemption_comment_matches_both_windows(). They are exported as part of the public test surface to keep the singleton-allowed exemption comment in sync with the actual window values. Removing them would break that arch test.
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit review found 13 blocking issues (2 critical, 11 warning). See inline comments. [verdict: changes_requested — GitHub self-review constraint prevented REQUEST_CHANGES state; review submitted as COMMENT]
5ea3d37 to
2741fec
Compare
…re dismissal Replaces _stale_check.py + _source_drift.py with two new cohesive modules: - cli/_install_info.py: pure classification + policy (InstallType, InstallInfo, detect_install, comparison_branch, dismissal_window, upgrade_command) - cli/_update_checks.py: runtime orchestration with Signal gatherers, unified _is_dismissed (time+version-delta expiry, no SHA keying), and run_update_checks() that emits at most one [Y/n] prompt per CLI invocation Also adds: - cli/_update.py + app.py `update` subcommand: first-class upgrade path - Doctor checks 15 (install_classification) and 16 (update_dismissal_state) - source_version_drift now uses network=True - docs/updating.md: branch-aware windows, escape hatches, autoskillit update - docs/installation.md: updated to 16 checks with new rows 13-16 + Updating section - Tests: test_install_info.py, test_update_checks.py, test_update_command.py - Version bump: 0.7.77 → 0.7.78 Fixes #758 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add module-level imports for _count_hook_registry_drift and _claude_settings_path in _update_checks.py (tests monkeypatched these but they were local imports inside functions) - Add module-level imports for detect_install, upgrade_command, terminal_guard in _update.py (same reason) - Update _is_dismissed docstring to mention '7 days' and '12 hours' windows (test_update_checks_docstring_describes_both_windows) - Update CLAUDE.md architecture section: replace deleted _stale_check and _source_drift entries with _install_info, _update, _update_checks - Rename docs/updating.md -> docs/update-checks.md (noun-phrase rule) - Update doc references from updating.md to update-checks.md - Fix doctor test patches: use autoskillit.cli._install_info.detect_install (doctor imports detect_install from _install_info, not _update_checks) - Fix test_check_source_version_drift_ok_when_cache_empty: assert 'unavailable' in message (doctor now uses network=True, message changed) - Delete test_check_source_version_drift_never_makes_network_call (contradicts network=True design; doctor now makes network calls) - Remove stale TestSyncRemovalCLI::test_update_command_does_not_exist (update command now exists as designed) - Fix HookDriftResult calls: remove 'ok' kwarg (not a NamedTuple field) - Update test_doc_counts: assert 18 doctor checks (16 numbered + 2 lettered) - Update schema convention allowlist line numbers (102/129 -> 104/131)
…unify snooze key - _update.py and _update_checks._run_update_sequence both called _verify_update_result(current, current, ...) — passing current as both current and latest. The snooze record now stores attempted_version from the fetched latest (via comparison_branch + _fetch_latest_version). - run_update_command now clears 'binary_snoozed' (consistent with writer) instead of 'update_snoozed' (stale key name). - Remove dead duplicate _INSTALL_FROM_INTEGRATION from _update_checks.py; canonical definition lives in _install_info.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…verage - Remove tautological `or 'source_drift' == sig.kind` arm in test_source_drift_signal_fires_when_commit_lags_ref (always True) - Strengthen test_yes_runs_upgrade_command_from_install_info_not_hardcoded to assert the exact command from upgrade_command(info) was invoked, not a trivially-satisfied string-match over any subprocess call - Add test_update_verifies_version_advance_and_warns_on_failure disk-state assertion: binary_snoozed.attempted_version must equal the fetched latest - Add find_source_repo behavioral tests: env-var override valid/invalid, CWD-walk pyproject.toml match, no-match returns None - Add test_check_source_version_drift_returns_ok_when_network_unavailable: verifies fail-open (Severity.OK) when resolve_reference_sha returns None Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…numbers - Patch _fetch_latest_version via autoskillit.cli._update_checks (the module that owns it) not _update (which lazy-imports it) - Update _LEGACY_JSON_WRITES line numbers: removal of the dead _INSTALL_FROM_INTEGRATION constant shifted the two _update_checks.py atomic_write sites from lines 104/131 to 103/130 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2741fec to
7a64133
Compare
Replace bare 'autoskillit' binary invocation with sys.executable -m autoskillit so the test resolves the CLI via the active venv rather than relying on PATH, matching the established pattern in test_recording_sigterm.py.
Summary
Replace the two independent startup update checks (
cli/_stale_check.py+cli/_source_drift.py) with a single unified update check that reads one sourceof truth from
direct_url.json(vcs_info.requested_revision), consolidates thethree existing prompts (binary / hook-drift / source-drift) into at most one
[Y/n] prompt per CLI invocation, and uses branch-aware dismissal windows
(7 days for stable/main, 12 hours for integration/editable).
The approach introduces two new cohesive modules and deletes both legacy check
modules:
cli/_install_info.py— pure classification + policy. OwnsInstallInfo,InstallType,detect_install(), and the three policy helperscomparison_branch(),dismissal_window(),upgrade_command(). No I/O.cli/_update_checks.py— runtime orchestration. Owns the GitHub fetch cache(ported from
_stale_check.py), source-repo resolution (ported from_source_drift.py), the three signal gatherers, the consolidated prompt, andthe single
run_update_checks()entry point called bymain().cli/_update.py+ a new@app.command updatesubcommand — first-class,install-type-aware upgrade path that does not require waiting for a prompt.
is_dev_mode(),_detect_install_type(),_DISMISS_WINDOW, and_is_drift_dismisseddisappear entirely. The~/.autoskillit/devmarker filebecomes a dead signal (no new reads). Dismissal state is a single unified
record under
state["update_prompt"]keyed by time + version-delta, never bySHA pair.
Architecture Impact
State Lifecycle Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 60, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000; classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; ENTRY(["● main() — autoskillit <cmd>"]) subgraph FIELD_CATS ["FIELD LIFECYCLE CATEGORIES"] direction LR FLD_INIT["INIT_ONLY (frozen=True)<br/>━━━━━━━━━━<br/>★ InstallInfo: install_type<br/>commit_id · requested_revision<br/>url · editable_source<br/>★ Signal: kind · message<br/>Python raises FrozenInstanceError<br/>on any reassignment attempt"] FLD_MUT["MUTABLE (disk-persisted)<br/>━━━━━━━━━━<br/>★ update_check.json<br/>update_prompt.dismissed_at<br/>update_prompt.dismissed_version<br/>update_prompt.conditions<br/>binary_snoozed.snoozed_at<br/>binary_snoozed.attempted_version"] FLD_CACHE["MUTABLE (TTL-cached)<br/>━━━━━━━━━━<br/>★ github_fetch_cache.json<br/>body · etag · cached_at<br/>TTL: 30 min (env override)<br/>ETag used for conditional GET"] FLD_DERIVED["DERIVED (computed, not stored)<br/>━━━━━━━━━━<br/>★ comparison_branch()<br/>7d window → releases/latest<br/>12h window → integration<br/>★ dismissal_window()<br/>★ upgrade_command()"] end subgraph ENTRY_GATES ["GATES 1–2: ENTRY GUARDS"] G_ENV["Gate 1: ENV / TTY Guard<br/>━━━━━━━━━━<br/>CLAUDECODE=1 → skip<br/>CI=1 → skip<br/>AUTOSKILLIT_SKIP_STALE_CHECK=1 → skip<br/>stdin or stdout not isatty() → skip"] G_INSTALL["★ detect_install() → InstallInfo<br/>━━━━━━━━━━<br/>reads: direct_url.json (pip metadata)<br/>returns frozen dataclass<br/>all 5 fields INIT_ONLY"] G_TYPE["Gate 2: Install Type<br/>━━━━━━━━━━<br/>LOCAL_EDITABLE → skip<br/>LOCAL_PATH → skip<br/>UNKNOWN → skip<br/>GIT_VCS → proceed"] end subgraph SIGNALS ["GATE 3: SIGNAL GATHERING (parallel, 3 paths)"] SIG_BIN["★ _binary_signal()<br/>━━━━━━━━━━<br/>comparison_branch(info) → target<br/>_fetch_latest_version(target)<br/>fires if latest > current<br/>reads/writes: github_fetch_cache.json"] SIG_HOOK["★ _hooks_signal()<br/>━━━━━━━━━━<br/>_count_hook_registry_drift()<br/>reads: Claude settings.json hooks key<br/>fires if missing > 0 or orphaned > 0<br/>no I/O side effects"] SIG_DRIFT["★ _source_drift_signal()<br/>━━━━━━━━━━<br/>resolve_reference_sha()<br/>git ls-remote / GitHub refs API<br/>fires if ref_sha ≠ commit_id<br/>reads/writes: github_fetch_cache.json"] end subgraph DISMISS_GATE ["GATE 4: ★ _is_dismissed() — TRIPLE-AXIS DISMISSAL CHECK"] DA["Axis A: Time Window<br/>━━━━━━━━━━<br/>dismissed_at + window > now<br/>7 days — stable · main · tag<br/>12 hours — integration<br/>window from dismissal_window(current_install)<br/>not stored install: branch-aware re-eval"] DB["Axis B: Version Delta<br/>━━━━━━━━━━<br/>current_version <= dismissed_version<br/>any version advance immediately<br/>overrides the time window<br/>forces re-prompt regardless"] DC["Axis C: Condition Match<br/>━━━━━━━━━━<br/>condition ∈ update_prompt.conditions<br/>per-signal granularity<br/>dismiss binary ≠ dismiss hooks<br/>dismiss hooks ≠ dismiss source_drift"] ALL3{"All 3 axes<br/>satisfied?"} end subgraph RESUME_TIERS ["RESUME DETECTION TIERS"] RT1["Tier 1: Suppressed<br/>━━━━━━━━━━<br/>All 3 axes pass<br/>signal silently filtered out<br/>no prompt, no output"] RT2["Tier 2: Version Expired<br/>━━━━━━━━━━<br/>current_version > dismissed_version<br/>axis B fails → re-prompt<br/>overrides remaining window"] RT3["Tier 3: Window Expired<br/>━━━━━━━━━━<br/>now >= dismissed_at + window<br/>axis A fails → re-prompt<br/>fresh 7d or 12h window starts"] RT4["Tier 4: Fresh / No State<br/>━━━━━━━━━━<br/>state = {} or key absent<br/>fail-open: all axes fail<br/>always prompt"] end subgraph MUTATIONS ["STATE MUTATION POINTS (write paths)"] M_DISMISS["★ N-Path: Write Dismissal<br/>━━━━━━━━━━<br/>update_prompt = {<br/> dismissed_at: now() UTC ISO-8601<br/> dismissed_version: current version<br/> conditions: [firing signal kinds]<br/>} → atomic_write"] M_UPGRADE["★ _run_update_sequence()<br/>━━━━━━━━━━<br/>upgrade_command(info) subprocess<br/>then: autoskillit install subprocess<br/>both receive SKIP envs injected<br/>prevents recursive update check"] M_SNOOZE["binary_snoozed write<br/>━━━━━━━━━━<br/>snoozed_at · attempted_version<br/>⚠ DEAD WRITE: key is written<br/>but never READ by _is_dismissed()<br/>has no suppression effect"] M_CLEAR["★ Success: Clear Dismissal<br/>━━━━━━━━━━<br/>pop update_prompt<br/>pop update_snoozed<br/>atomic_write(cleaned state)<br/>next startup re-checks from scratch"] M_FETCH["Fetch Cache Write<br/>━━━━━━━━━━<br/>body · etag · cached_at<br/>written on HTTP 200 or 304<br/>atomic_write to cache file"] end STORAGE[("★ ~/.autoskillit/<br/>update_check.json<br/>atomic writes via os.replace")] CACHE_FILE[("★ ~/.autoskillit/<br/>github_fetch_cache.json<br/>read: TTL-gated")] SILENT(["silent return"]) PROMPT{"★ Consolidated prompt<br/>Update now? [Y/n]<br/>single prompt for all signals"} %% ENTRY FLOW %% ENTRY --> G_ENV G_ENV -->|"headless / CI / skip-flags / no-TTY"| SILENT G_ENV --> G_INSTALL G_INSTALL --> G_TYPE G_TYPE -->|"LOCAL · UNKNOWN"| SILENT G_TYPE -->|"GIT_VCS"| SIG_BIN G_TYPE --> SIG_HOOK G_TYPE --> SIG_DRIFT %% SIGNALS → DISMISSAL GATE %% SIG_BIN --> DA SIG_HOOK --> DA SIG_DRIFT --> DA DA --> DB --> DC --> ALL3 %% RESUME TIER ROUTING %% ALL3 -->|"all pass: suppressed"| RT1 ALL3 -->|"axis B fails: version advanced"| RT2 ALL3 -->|"axis A fails: window expired"| RT3 ALL3 -->|"no prior state"| RT4 RT1 --> SILENT RT2 --> PROMPT RT3 --> PROMPT RT4 --> PROMPT %% ACTION PATHS %% PROMPT -->|"N / other → dismiss"| M_DISMISS PROMPT -->|"Y / Enter → upgrade"| M_UPGRADE M_UPGRADE -->|"version advanced"| M_CLEAR M_UPGRADE -->|"version unchanged"| M_SNOOZE %% WRITES TO STORAGE %% M_DISMISS --> STORAGE M_SNOOZE --> STORAGE M_CLEAR --> STORAGE SIG_BIN --> M_FETCH SIG_DRIFT --> M_FETCH M_FETCH --> CACHE_FILE %% LIFECYCLE CATEGORY READ ANNOTATIONS %% FLD_MUT -.->|"READ at startup via _read_dismiss_state()"| STORAGE FLD_CACHE -.->|"READ if within TTL"| CACHE_FILE %% CLASS ASSIGNMENTS %% class ENTRY cli; class G_ENV detector; class G_INSTALL newComponent; class G_TYPE detector; class FLD_INIT detector; class FLD_MUT phase; class FLD_CACHE stateNode; class FLD_DERIVED output; class SIG_BIN,SIG_HOOK,SIG_DRIFT newComponent; class DA,DB,DC detector; class ALL3 handler; class RT1 output; class RT2,RT3,RT4 gap; class M_DISMISS,M_CLEAR newComponent; class M_UPGRADE handler; class M_SNOOZE gap; class M_FETCH stateNode; class STORAGE,CACHE_FILE stateNode; class SILENT,PROMPT terminal;Development Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%% flowchart TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; %% ── BUILD ─────────────────────────────────────────────────── %% subgraph BUILD ["BUILD TOOLING"] direction TB PYPROJECT["● pyproject.toml<br/>━━━━━━━━━━<br/>hatchling build-backend<br/>v0.7.78 (+packaging≥23.0)<br/>uv.lock updated"] TASKFILE["Taskfile.yml<br/>━━━━━━━━━━<br/>task test-all (lint+test)<br/>task test-check (CI/MCP)<br/>task sync-plugin-version"] end %% ── SOURCE ────────────────────────────────────────────────── %% subgraph SOURCE ["SOURCE: src/autoskillit/cli/ (L3)"] direction TB INSTALL_INFO["★ _install_info.py<br/>━━━━━━━━━━<br/>InstallType StrEnum<br/>InstallInfo dataclass<br/>detect_install()<br/>comparison_branch()<br/>dismissal_window()<br/>upgrade_command()<br/><i>Pure — no I/O, no network</i>"] UPDATE_CHECKS["★ _update_checks.py<br/>━━━━━━━━━━<br/>Signal(binary/hooks/source_drift)<br/>_read/_write_dismiss_state()<br/>_fetch_with_cache() + ETag<br/>_binary_signal()<br/>_hooks_signal()<br/>_source_drift_signal()<br/>run_update_checks()"] UPDATE["★ _update.py<br/>━━━━━━━━━━<br/>run_update_command()<br/>upgrade + autoskillit install<br/>dismissal clear on success"] APP["● app.py<br/>━━━━━━━━━━<br/>+update subcommand<br/>wired to cyclopts App"] DOCTOR["● _doctor.py<br/>━━━━━━━━━━<br/>install_info check<br/>(network=False mode)"] end %% ── QUALITY GATES ─────────────────────────────────────────── %% subgraph QUALITY ["CODE QUALITY GATES"] direction TB PRECOMMIT["pre-commit hooks<br/>━━━━━━━━━━<br/>ruff format (auto-fix)<br/>ruff lint (auto-fix)<br/>mypy type-check<br/>uv lock check<br/>gitleaks secret scan"] IMPORTLINT["import-linter<br/>━━━━━━━━━━<br/>IL-001…IL-007 contracts<br/>L0/L1/L2/L3 layer isolation<br/>cli must not import upward"] ARCH_RULES["● tests/arch/_rules.py<br/>━━━━━━━━━━<br/>AST rule definitions"] ARCH_ISO["● test_subpackage_isolation.py<br/>━━━━━━━━━━<br/>layer contract assertions<br/>new modules registered"] end %% ── TESTS ─────────────────────────────────────────────────── %% subgraph TESTS ["TEST FRAMEWORK — pytest-xdist -n 4"] direction TB T_INSTALL["★ test_install_info.py<br/>━━━━━━━━━━<br/>detect_install() variants<br/>comparison_branch() policy<br/>dismissal_window() policy<br/>upgrade_command() policy"] T_CHECKS["★ test_update_checks.py<br/>━━━━━━━━━━<br/>UC-1 env guards<br/>dismiss state I/O<br/>fetch cache + ETag<br/>signal gathering<br/>_is_dismissed() axes<br/>run_update_checks() e2e"] T_CMD["★ test_update_command.py<br/>━━━━━━━━━━<br/>update --help registered<br/>upgrade cmd per revision<br/>autoskillit install called<br/>skip-env propagated<br/>dismissal cleared on success<br/>UNKNOWN → SystemExit(2)"] T_DOCTOR["● test_doctor.py<br/>━━━━━━━━━━<br/>install_info check updated"] T_TTY["● test_input_tty_contracts.py<br/>━━━━━━━━━━<br/>TTY guard coverage"] T_ENV["● test_subprocess_env_contracts.py<br/>━━━━━━━━━━<br/>skip-env vars verified"] T_DOCS["● test_doc_counts.py<br/>━━━━━━━━━━<br/>+1 doc file registered"] T_SCHEMA["● test_schema_version_convention.py<br/>━━━━━━━━━━<br/>v0.7.78 bump verified"] end %% ── ENTRY POINTS ──────────────────────────────────────────── %% subgraph ENTRY ["ENTRY POINTS"] direction LR CLI_SERVE["autoskillit serve<br/>━━━━━━━━━━<br/>MCP server (default)"] CLI_UPDATE["★ autoskillit update<br/>━━━━━━━━━━<br/>branch-aware upgrade<br/>+ hook sync"] CLI_DOCTOR["autoskillit doctor<br/>━━━━━━━━━━<br/>16 project health checks<br/>(incl. install_info check)"] end %% ── DISK ARTIFACTS ─────────────────────────────────────────── %% DISMISS_STATE["~/.autoskillit/update_check.json<br/>━━━━━━━━━━<br/>dismissed_at / dismissed_version<br/>conditions list (branch-aware TTL)"] FETCH_CACHE["~/.autoskillit/github_fetch_cache.json<br/>━━━━━━━━━━<br/>ETag + body per URL<br/>30-min TTL (rate-limit defense)"] %% ── EXTERNAL ───────────────────────────────────────────────── %% GITHUB_API["GitHub API<br/>━━━━━━━━━━<br/>releases/latest<br/>pyproject.toml@integration<br/>git refs/heads/{rev}"] %% ── FLOW ──────────────────────────────────────────────────── %% PYPROJECT --> SOURCE TASKFILE --> QUALITY TASKFILE --> TESTS INSTALL_INFO -->|"imported by"| UPDATE_CHECKS INSTALL_INFO -->|"imported by"| UPDATE INSTALL_INFO -->|"imported by"| DOCTOR UPDATE_CHECKS -->|"imported by"| UPDATE APP -->|"calls"| UPDATE_CHECKS APP -->|"calls"| UPDATE SOURCE --> QUALITY QUALITY --> TESTS TESTS --> ENTRY UPDATE_CHECKS <-->|"reads/writes"| DISMISS_STATE UPDATE_CHECKS <-->|"reads/writes"| FETCH_CACHE FETCH_CACHE <-->|"conditional GET + ETag"| GITHUB_API T_INSTALL -->|"tests"| INSTALL_INFO T_CHECKS -->|"tests"| UPDATE_CHECKS T_CMD -->|"tests"| UPDATE T_DOCTOR -->|"tests"| DOCTOR T_ENV -->|"tests"| UPDATE_CHECKS APP --> CLI_UPDATE APP --> CLI_SERVE APP --> CLI_DOCTOR %% ── CLASS ASSIGNMENTS ─────────────────────────────────────── %% class PYPROJECT,TASKFILE phase; class INSTALL_INFO,UPDATE,UPDATE_CHECKS newComponent; class APP,DOCTOR handler; class PRECOMMIT,IMPORTLINT detector; class ARCH_RULES,ARCH_ISO detector; class T_INSTALL,T_CHECKS,T_CMD newComponent; class T_DOCTOR,T_TTY,T_ENV,T_DOCS,T_SCHEMA handler; class CLI_SERVE,CLI_DOCTOR cli; class CLI_UPDATE newComponent; class DISMISS_STATE,FETCH_CACHE stateNode; class GITHUB_API output;Scenarios Diagram
%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 55, 'curve': 'basis'}}}%% flowchart LR %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,stroke-width:2px,color:#fff; classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff; classDef integration fill:#c62828,stroke:#ef9a9a,stroke-width:2px,color:#fff; classDef terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; %% ───────────────────────────────────────────────────────── %% SCENARIO 1: Startup update check — user accepts update %% ───────────────────────────────────────────────────────── subgraph S1 ["S1 · Startup Check → Accept & Update"] direction LR S1_CLI["● app.py main()<br/>━━━━━━━━━━<br/>any non-serve cmd<br/>calls run_update_checks()"] S1_GATE["★ run_update_checks()<br/>━━━━━━━━━━<br/>guards: CI / TTY / env<br/>reads dismiss state"] S1_DETECT["★ detect_install()<br/>━━━━━━━━━━<br/>reads direct_url.json<br/>→ InstallInfo"] S1_SIG["★ signal gatherers<br/>━━━━━━━━━━<br/>_binary_signal()<br/>_hooks_signal()<br/>_source_drift_signal()"] S1_CACHE["★ _fetch_with_cache()<br/>━━━━━━━━━━<br/>30-min disk TTL<br/>ETag / GitHub API"] S1_FILTER["★ _is_dismissed()<br/>━━━━━━━━━━<br/>time-window check<br/>version-delta check"] S1_PROMPT["★ [Y/n] prompt<br/>━━━━━━━━━━<br/>bullets each signal<br/>one prompt only"] S1_RUN["★ _run_update_sequence()<br/>━━━━━━━━━━<br/>upgrade_command() subprocess<br/>+ autoskillit install"] S1_VERIFY["★ _verify_update_result()<br/>━━━━━━━━━━<br/>importlib.metadata<br/>confirm version advanced"] end S1_CLI -->|"calls"| S1_GATE S1_GATE -->|"classify"| S1_DETECT S1_DETECT -->|"InstallInfo"| S1_SIG S1_SIG -->|"fetch version / SHA"| S1_CACHE S1_SIG -->|"filter dismissed"| S1_FILTER S1_FILTER -->|"firing signals"| S1_PROMPT S1_PROMPT -->|"Y"| S1_RUN S1_RUN -->|"verify"| S1_VERIFY %% ───────────────────────────────────────────────────────── %% SCENARIO 2: Startup update check — user dismisses %% ───────────────────────────────────────────────────────── subgraph S2 ["S2 · Startup Check → Dismiss"] direction LR S2_PROMPT["★ [Y/n] prompt<br/>━━━━━━━━━━<br/>fires after guard/filter<br/>in run_update_checks()"] S2_WRITE["★ _write_dismiss_state()<br/>━━━━━━━━━━<br/>conditions: [binary,hooks,drift]<br/>dismissed_version, dismissed_at"] S2_STATE["update_check.json<br/>━━━━━━━━━━<br/>~/.autoskillit/<br/>branch-aware window key"] S2_NEXT["next invocation<br/>━━━━━━━━━━<br/>_is_dismissed() returns True<br/>prompt suppressed"] end S2_PROMPT -->|"N"| S2_WRITE S2_WRITE -->|"atomic_write"| S2_STATE S2_STATE -->|"read on next run"| S2_NEXT %% ───────────────────────────────────────────────────────── %% SCENARIO 3: autoskillit update (explicit command) %% ───────────────────────────────────────────────────────── subgraph S3 ["S3 · autoskillit update (explicit)"] direction LR S3_CLI["● app.py update()<br/>━━━━━━━━━━<br/>subcommand handler<br/>calls run_update_command()"] S3_DETECT["★ detect_install()<br/>━━━━━━━━━━<br/>reads direct_url.json<br/>→ InstallInfo"] S3_CMD["★ upgrade_command()<br/>━━━━━━━━━━<br/>branch-aware:<br/>uv tool upgrade / force reinstall"] S3_PROC["subprocess run<br/>━━━━━━━━━━<br/>upgrade + autoskillit install<br/>SKIP_STALE_CHECK=1"] S3_VERIFY["★ _verify_update_result()<br/>━━━━━━━━━━<br/>importlib.metadata check"] S3_CLEAR["★ _write_dismiss_state()<br/>━━━━━━━━━━<br/>pop update_prompt<br/>clear stale dismissal"] end S3_CLI -->|"calls"| S3_DETECT S3_DETECT -->|"InstallInfo"| S3_CMD S3_CMD -->|"command list"| S3_PROC S3_PROC -->|"on success"| S3_VERIFY S3_VERIFY -->|"version advanced"| S3_CLEAR %% ───────────────────────────────────────────────────────── %% SCENARIO 4: autoskillit doctor — install & dismissal audit %% ───────────────────────────────────────────────────────── subgraph S4 ["S4 · autoskillit doctor (install audit, no network)"] direction LR S4_CLI["● _doctor.py checks<br/>━━━━━━━━━━<br/>check_source_drift<br/>check_install_classification<br/>check_update_dismissal_state"] S4_DETECT["★ detect_install()<br/>━━━━━━━━━━<br/>classify from direct_url.json<br/>UNKNOWN → WARNING"] S4_SHA["★ resolve_reference_sha()<br/>━━━━━━━━━━<br/>network=False (doctor mode)<br/>reads disk cache only"] S4_CACHE["★ _api_sha() cache read<br/>━━━━━━━━━━<br/>no HTTP; reads existing<br/>github_fetch_cache.json"] S4_DISMISS["★ _read_dismiss_state()<br/>━━━━━━━━━━<br/>reads update_check.json<br/>reports expiry date"] S4_RESULT["DoctorResult<br/>━━━━━━━━━━<br/>OK / WARNING / ERROR<br/>with human-readable message"] end S4_CLI -->|"classify"| S4_DETECT S4_CLI -->|"drift check"| S4_SHA S4_SHA -->|"cache-only lookup"| S4_CACHE S4_CLI -->|"dismissal window"| S4_DISMISS S4_DETECT -->|"result"| S4_RESULT S4_CACHE -->|"SHA or None"| S4_RESULT S4_DISMISS -->|"expiry info"| S4_RESULT %% CLASS ASSIGNMENTS class S1_CLI,S3_CLI,S4_CLI cli; class S1_GATE,S1_SIG,S1_FILTER,S1_PROMPT,S2_PROMPT,S1_VERIFY,S3_VERIFY newComponent; class S1_DETECT,S3_DETECT,S4_DETECT,S3_CMD,S4_SHA newComponent; class S1_CACHE,S4_CACHE,S1_RUN,S3_PROC integration; class S1_WRITE,S2_WRITE,S3_CLEAR,S4_DISMISS newComponent; class S2_STATE stateNode; class S2_NEXT phase; class S2_WRITE,S3_CLEAR handler; class S4_RESULT output;Closes #758
Implementation Plan
Plan file:
/home/talon/projects/autoskillit-runs/impl-20260412-120704-922893/.autoskillit/temp/make-plan/cli_update_prompts_single_source_of_truth_plan_2026-04-12_121616.md🤖 Generated with Claude Code via AutoSkillit
Token Usage Summary