Rectify: CLI Startup Update Prompt — Terminal Freeze Immunity#783
Rectify: CLI Startup Update Prompt — Terminal Freeze Immunity#783Trecek wants to merge 13 commits intointegrationfrom
Conversation
Introduce _timed_input.py with two architectural primitives that make the class of "silent blocking + invisible prompt + unbounded input" bugs structurally impossible: - timed_prompt(): composes TTY guard, ANSI formatting, and select.select-based timeout into a single function - status_line(): pre-flight feedback primitive for printing status before blocking I/O Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the new env var to AUTOSKILLIT_PRIVATE_ENV_VARS so it is stripped from subprocess environments via build_sanitized_env(), preventing it from leaking into test subprocesses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add status_line("Checking for updates...") before signal gathering
to eliminate silent pre-prompt network I/O
- Replace raw print()+input("") with timed_prompt() (30s timeout)
- On timeout: return without dismissal so prompt reappears next time
- Add AUTOSKILLIT_FORCE_UPDATE_CHECK env var override for LOCAL_EDITABLE
- Remove dead _SNOOZE_WINDOW constant (never referenced)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 15 raw input() calls across 6 CLI files with timed_prompt(): - _cook.py: cook() launch confirm - _init_helpers.py: recipe choice, test command, GitHub repo, secret scan - _onboarding.py: onboarding menu (3 prompts) - _workspace.py: directory removal confirms (2 prompts) - app.py: recipe selection, subset/pack gates, launch confirm Each call site inherits TTY guard, ANSI formatting, and bounded timeout. Removes redundant _require_interactive_stdin() calls at each site. Also removes .lower() from timed_prompt() so callers handle normalization. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the old _TTY_EXEMPT_FUNCTIONS / _require_interactive_stdin check with a stricter contract: no raw input() calls anywhere in src/autoskillit/cli/ except inside _timed_input.py itself. This makes the entire class of "silent blocking + invisible prompt + unbounded input" bugs structurally impossible to reintroduce. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- test_timed_prompt_returns_default_on_timeout (1b) - test_timed_prompt_applies_ansi_formatting (1c) - test_timed_prompt_respects_no_color (1d) - test_run_update_checks_emits_status_before_network (1e) - test_force_update_check_env_overrides_local_editable (1f) Also update _setup_run_checks helper to mock select.select for timed_prompt compatibility, and clear AUTOSKILLIT_FORCE_UPDATE_CHECK in existing guard tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
timed_prompt() used select.select(sys.stdin) which requires fileno(), crashing in pytest's captured stdin and piped environments. Catch OSError/TypeError/ValueError and fall back to blocking input(). Also fixes: - Onboarding tests: add isatty mock for _require_interactive_stdin gate - AST rules: exempt _timed_input.py from no-print rule (user-facing CLI) - Subpackage isolation: bump cli/ limit 17→18 for new _timed_input.py - CLAUDE.md: document _timed_input.py in architecture section - Schema convention: update shifted line numbers for JSON write sites - test_cook: adapt prompt assertion to timed_prompt's print-then-input Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Under xdist parallel load (4 workers, ~7850 tests), the subprocess started by test_sigterm_writes_scenario_json never fully starts within 5 seconds — empty stdout/stderr confirms the process hasn't initialized before SIGTERM arrives. Increasing the deadline to 15s tolerates slow subprocess startup under heavy CPU contention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit PR Review — Verdict: changes_requested
| import sys | ||
|
|
||
| from autoskillit.cli._ansi import supports_color | ||
| from autoskillit.cli._init_helpers import _require_interactive_stdin |
There was a problem hiding this comment.
[critical] arch: _timed_input.py imports _require_interactive_stdin from _init_helpers.py at module level, while _init_helpers.py lazily imports timed_prompt from _timed_input.py. This creates a latent circular dependency. As the foundational prompt primitive, _timed_input.py must not depend on peer CLI modules. Inline the TTY guard or extract _require_interactive_stdin to a shared utility.
There was a problem hiding this comment.
Investigated — this is intentional. _init_helpers.py has NO top-level import of _timed_input. All four timed_prompt imports are strictly inside function bodies (lazy): _prompt_recipe_choice, _prompt_test_command, _prompt_github_repo, _check_secret_scanning. Python's import machinery resolves these lazily only when those functions are called, not when _timed_input is first loaded. No circular import cycle exists at module load time.
|
|
||
| assert len(prompts_seen) == 1, "input() should be called exactly once (confirmation)" | ||
| assert "Launch session" in prompts_seen[0] | ||
| assert len(input_calls) == 1, "input() should be called exactly once (confirmation)" |
There was a problem hiding this comment.
[warning] tests: The assertion "Launch session" in prompts_seen[0] was dropped. Since timed_prompt calls input(""), the old check on input() args cannot work — but the prompt content should be verified via captured print output instead of being dropped entirely.
There was a problem hiding this comment.
Investigated — this is intentional. timed_prompt() does not call status_line(); the prompt text is passed to print() with ANSI formatting, then input('') is called with no arguments. The suggested mechanism (asserting via status_line mock) would not work for this code path. status_line() is a separate utility in _timed_input.py that timed_prompt() does not invoke.
| # Consolidated prompt | ||
| from autoskillit.cli._timed_input import timed_prompt | ||
|
|
||
| _TIMEOUT_SENTINEL = "__timeout__" |
There was a problem hiding this comment.
[warning] cohesion: _TIMEOUT_SENTINEL string sentinel is asymmetric with all other timed_prompt call sites that treat the returned default as a valid answer. If timeout-vs-empty-input distinction is needed here, consider adding a return-type flag to timed_prompt itself rather than leaking sentinel detection into callers.
There was a problem hiding this comment.
Investigated — this is intentional. The _TIMEOUT_SENTINEL implements a deliberate 3-way branch: (1) timeout → sentinel returned → early return WITHOUT writing dismissal, so the prompt reappears next invocation; (2) empty/y/yes → update runs; (3) n/no → dismissal record written. Simplifying to default='' would conflate timeout with user pressing Enter (the Y path), triggering an unwanted update. Commit 9b7dbb5 explicitly documents this contract.
| label | ||
| Human-readable label for the TTY-guard error message. | ||
| """ | ||
| _require_interactive_stdin(label) |
There was a problem hiding this comment.
[warning] defense: TTY guard fires unconditionally, including when timeout=0 (documented as "user explicitly initiated"). A user-initiated prompt could legitimately come from piped input. Should timeout=0 callers bypass the guard?
There was a problem hiding this comment.
Investigated — this is intentional. The docstring documents timeout=0 as 'use only for prompts the user explicitly initiated', presupposing a TTY. The module-level invariant is that ALL timed_prompt calls require an interactive terminal. Zero call sites in the codebase use timeout=0, making this a theoretical concern about an untested path. The TTY guard is coherent with the design contract.
| assert received_headers["Authorization"] == "Bearer my-secret-token" | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
[warning] cohesion: timed_prompt unit tests (timeout, ANSI, NO_COLOR) live in test_update_checks.py rather than a dedicated test_timed_input.py. Feature locality: the primitives tests should co-locate with its source module name.
There was a problem hiding this comment.
Valid observation — flagged for design decision. Moving timed_prompt unit tests to a dedicated test_timed_input.py would improve feature locality, but requires a human decision on whether the added file is worth the disruption to the established module layout. No test_timed_input.py exists currently.
Trecek
left a comment
There was a problem hiding this comment.
AutoSkillit review found 9 blocking issues (1 critical, 8 warning). See inline comments.
Hoist timed_prompt import to a single site at the top of order() instead of three aliased lazy imports (_tp, _tp2, _tp3) in conditional branches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents IndexError with no diagnostic when signal_calls is empty by asserting its length before accessing signal_calls[0]. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The _require_interactive_stdin tests are live regression coverage, not legacy — remove the inaccurate "superseded" apology framing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The existing AST walk only checked FunctionDef/AsyncFunctionDef nodes, silently skipping module-level code. Add a second pass over top-level statements to detect raw input() calls outside any function or class. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The hoisted rel_path computation now uses an absolute _SRC_DIR derived
from __file__ instead of relative Path("src"), fixing ValueError when
py_file is absolute.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
The CLI startup update check (
run_update_checks()) has produced five separate issues because it lacks architectural primitives that enforce two invariants: (1) every blocking call must be preceded by visual output, and (2) every user prompt must have bounded duration and consistent formatting. The codebase has ad-hoc solutions to both —_require_interactive_stdin()for TTY guards,supports_color()for ANSI formatting — but no primitive that composes them. The result is that any newinput()call site can silently violate both invariants without test failure.This plan introduces two architectural primitives and a structural test contract that make the entire class of "silent blocking + invisible prompt + unbounded input" bugs structurally impossible to reintroduce — not just in
_update_checks.py, but across the entire CLI layer.Architecture Impact
Process Flow 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 terminal fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; %% TERMINALS %% START([START: CLI Invocation]) COMPLETE([COMPLETE: Action Proceeds]) ABORT([ABORT: User Declined / Timeout]) ERROR([ERROR: SystemExit 1]) %% ── CLI ENTRY POINTS ── %% subgraph CLIEntry ["CLI Entry Points (callers)"] direction TB ORDER["● app.py::order<br/>━━━━━━━━━━<br/>Recipe select, subset/pack<br/>gates, launch confirm"] COOK["● _cook.py::cook<br/>━━━━━━━━━━<br/>Session launch confirm"] INIT["● _init_helpers.py<br/>━━━━━━━━━━<br/>Recipe, test cmd, repo,<br/>secret scan prompts"] ONBOARD["● _onboarding.py<br/>━━━━━━━━━━<br/>First-run menu [A-E]"] UPDATE["● _update_checks.py<br/>━━━━━━━━━━<br/>Startup update prompt<br/>timeout=30s"] WORKSPACE["● _workspace.py<br/>━━━━━━━━━━<br/>Destructive clean confirm"] end %% ── TIMED PROMPT CORE ── %% subgraph TimedPrompt ["★ _timed_input.py::timed_prompt()"] direction TB TTY_CHECK{"TTY Guard<br/>━━━━━━━━━━<br/>stdin.isatty()?"} ANSI["ANSI Format<br/>━━━━━━━━━━<br/>Bold prompt + dim hint<br/>respects NO_COLOR"] SELECT{"select.select()<br/>━━━━━━━━━━<br/>stdin readable<br/>within timeout?"} INPUT["input('')<br/>━━━━━━━━━━<br/>Blocking read<br/>+ strip()"] EOF_CATCH{"EOFError?<br/>━━━━━━━━━━<br/>stdin closed?"} end %% ── UPDATE CHECK FLOW ── %% subgraph UpdateFlow ["● Update Check Flow"] direction TB STATUS_LINE["● status_line()<br/>━━━━━━━━━━<br/>Pre-flight feedback"] UPDATE_DECIDE{"Response?<br/>━━━━━━━━━━<br/>timeout / y / n"} RUN_UPGRADE["Run Upgrade<br/>━━━━━━━━━━<br/>_run_update_sequence()"] WRITE_DISMISS["Write Dismissal<br/>━━━━━━━━━━<br/>Branch-aware window<br/>7d stable / 12h dev"] end %% ── ONBOARDING FLOW ── %% subgraph OnboardFlow ["● Onboarding Flow"] direction TB ONBOARD_ASK{"Help getting<br/>started? [Y/n]"} MENU_CHOICE{"Menu [A-E]<br/>━━━━━━━━━━<br/>A=setup B=issue<br/>C=init D=recipe E=skip"} ISSUE_REF["Issue URL prompt<br/>━━━━━━━━━━<br/>For option B"] end %% ── DESTRUCTIVE CONFIRM ── %% subgraph DestructConfirm ["● Destructive Operation Gates"] direction TB CLEAN_RUNS{"Remove stale<br/>runs? [y/N]<br/>━━━━━━━━━━<br/>default=n"} CLEAN_WTS{"Remove stale<br/>worktrees? [y/N]<br/>━━━━━━━━━━<br/>default=n"} end %% ── TEST ENFORCEMENT ── %% subgraph Enforcement ["● Structural Test Enforcement"] direction TB AST_SCAN["● test_input_tty_contracts<br/>━━━━━━━━━━<br/>AST walk: no raw input()<br/>in cli/ except _timed_input"] ARCH_RULES["● _rules.py<br/>━━━━━━━━━━<br/>Subpackage isolation<br/>boundary checks"] end %% ── FLOW CONNECTIONS ── %% %% Entry to timed_prompt START --> CLIEntry ORDER -->|"all 4 prompts"| TTY_CHECK COOK -->|"launch confirm"| TTY_CHECK INIT -->|"5 prompts"| TTY_CHECK ONBOARD -->|"3 prompts"| TTY_CHECK UPDATE -->|"via status_line"| STATUS_LINE UPDATE -->|"update confirm"| TTY_CHECK WORKSPACE -->|"2 confirms"| TTY_CHECK %% timed_prompt internal flow TTY_CHECK -->|"not TTY"| ERROR TTY_CHECK -->|"is TTY"| ANSI ANSI --> SELECT SELECT -->|"timeout=0: skip"| INPUT SELECT -->|"ready"| INPUT SELECT -->|"not ready"| ABORT SELECT -.->|"OSError/TypeError:<br/>fallback"| INPUT INPUT --> EOF_CATCH EOF_CATCH -->|"yes"| ABORT EOF_CATCH -->|"no: user input"| COMPLETE %% Update check specific flow STATUS_LINE --> UPDATE_DECIDE UPDATE_DECIDE -->|"__timeout__"| ABORT UPDATE_DECIDE -->|"y / Enter"| RUN_UPGRADE UPDATE_DECIDE -->|"n / other"| WRITE_DISMISS RUN_UPGRADE --> COMPLETE WRITE_DISMISS --> ABORT %% Onboarding flow ONBOARD_ASK -->|"n/no"| ABORT ONBOARD_ASK -->|"y/Enter"| MENU_CHOICE MENU_CHOICE -->|"B"| ISSUE_REF MENU_CHOICE -->|"A/C/D"| COMPLETE MENU_CHOICE -->|"E/other"| ABORT ISSUE_REF --> COMPLETE %% Destructive confirms CLEAN_RUNS -->|"y"| COMPLETE CLEAN_RUNS -->|"n/timeout"| ABORT CLEAN_WTS -->|"y"| COMPLETE CLEAN_WTS -->|"n/timeout"| ABORT %% Enforcement (standalone) AST_SCAN -.->|"enforces"| TTY_CHECK ARCH_RULES -.->|"validates"| TimedPrompt %% CLASS ASSIGNMENTS %% class START,COMPLETE,ABORT,ERROR terminal; class ORDER,COOK,INIT,ONBOARD,UPDATE,WORKSPACE cli; class TTY_CHECK,SELECT,EOF_CATCH,UPDATE_DECIDE,ONBOARD_ASK,MENU_CHOICE,CLEAN_RUNS,CLEAN_WTS stateNode; class ANSI,INPUT,STATUS_LINE,RUN_UPGRADE,WRITE_DISMISS,ISSUE_REF handler; class AST_SCAN,ARCH_RULES detector;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; START([CLI Entry: order / cook / init]) %% ───── GATE: TTY + TIMEOUT PRIMITIVES ───── %% subgraph TimedInput ["★ TIMED INPUT PRIMITIVES (_timed_input.py)"] direction TB TTY_GUARD["★ _require_interactive_stdin<br/>━━━━━━━━━━<br/>FAIL-FAST: SystemExit 1<br/>if not sys.stdin.isatty"] TIMED_PROMPT["★ timed_prompt<br/>━━━━━━━━━━<br/>3 invariants composed:<br/>TTY guard → ANSI fmt → select.select"] STATUS_LINE["★ status_line<br/>━━━━━━━━━━<br/>Pre-flight feedback<br/>before blocking I/O"] end %% ───── INIT_ONLY FIELDS ───── %% subgraph InitOnly ["INIT_ONLY (frozen after construction)"] direction TB SIGNAL["● Signal<br/>━━━━━━━━━━<br/>kind: binary|hooks|source_drift<br/>message: str<br/>FROZEN dataclass"] INSTALL_INFO["InstallInfo<br/>━━━━━━━━━━<br/>install_type, commit_id<br/>requested_revision, url<br/>FROZEN dataclass"] SCAN_RESULT["● _ScanResult<br/>━━━━━━━━━━<br/>passed: bool<br/>bypass_accepted: bool<br/>FROZEN NamedTuple"] end %% ───── MUTABLE STATE ───── %% subgraph MutableState ["MUTABLE (written via atomic_write)"] direction TB DISMISS_STATE["● dismiss_state<br/>━━━━━━━━━━<br/>update_check.json<br/>dismissed_at, dismissed_version<br/>conditions list"] FETCH_CACHE["fetch_cache<br/>━━━━━━━━━━<br/>github_fetch_cache.json<br/>body, etag, cached_at<br/>30-min TTL"] STATE_YAML[".state.yaml<br/>━━━━━━━━━━<br/>safety.secret_scan_bypass_accepted<br/>ISO datetime"] end %% ───── INIT_PRESERVE MARKERS ───── %% subgraph Markers ["INIT_PRESERVE (idempotent markers)"] direction TB ONBOARDED["● .onboarded marker<br/>━━━━━━━━━━<br/>Empty file, existence-only<br/>Written ONLY after cook<br/>session exits 0"] SESSION_ID["session_id<br/>━━━━━━━━━━<br/>Resume target from<br/>find_latest_session_id<br/>Preserved on --resume"] end %% ───── DERIVED STATE ───── %% subgraph Derived ["DERIVED (computed, not stored)"] direction TB IS_FIRST_RUN["● is_first_run<br/>━━━━━━━━━━<br/>config.yaml exists AND<br/>.onboarded absent AND<br/>recipes empty AND<br/>no skill overrides"] IS_DISMISSED["● _is_dismissed<br/>━━━━━━━━━━<br/>Two-axis expiry:<br/>time window + version delta"] end %% ───── VALIDATION GATES ───── %% subgraph Gates ["VALIDATION GATES"] direction TB GATE_TTY{"TTY check<br/>━━━━━━━━━━<br/>stdin.isatty AND<br/>stdout.isatty"} GATE_TIMEOUT{"select.select<br/>━━━━━━━━━━<br/>Bounded wait<br/>0 = indefinite"} GATE_SECRET["● _check_secret_scanning<br/>━━━━━━━━━━<br/>Scanner found OR<br/>exact phrase match"] GATE_DISMISS{"_is_dismissed<br/>━━━━━━━━━━<br/>7d stable / 12h integration<br/>+ version-delta expiry"} end %% ───── TIMEOUT OUTCOME ROUTING ───── %% subgraph Outcomes ["TIMEOUT OUTCOME ROUTING"] direction TB ON_RESPONSE["User responds<br/>━━━━━━━━━━<br/>input stripped → caller"] ON_TIMEOUT["Timeout fires<br/>━━━━━━━━━━<br/>Returns default param<br/>NO state written"] ON_EOF["EOFError<br/>━━━━━━━━━━<br/>Returns default param<br/>Pipe/CI safe"] ON_SELECT_FAIL["select fails<br/>━━━━━━━━━━<br/>OSError/TypeError<br/>Fallback: blocking input"] end %% ───── STRUCTURAL ENFORCEMENT ───── %% subgraph Enforcement ["STRUCTURAL CONTRACT ENFORCEMENT (tests)"] direction TB AST_SCAN["● test_no_raw_input<br/>━━━━━━━━━━<br/>AST scan: only _timed_input.py<br/>may call input directly"] BEHAV_TESTS["● test_*_noninteractive_exits<br/>━━━━━━━━━━<br/>cook, init, workspace clean<br/>must SystemExit on non-TTY"] end %% ═══════ FLOW ═══════ %% START --> GATE_TTY GATE_TTY -->|"pass"| TTY_GUARD GATE_TTY -->|"fail"| FAIL_EXIT(["SystemExit 1"]) TTY_GUARD --> STATUS_LINE STATUS_LINE --> TIMED_PROMPT TIMED_PROMPT --> GATE_TIMEOUT GATE_TIMEOUT -->|"ready"| ON_RESPONSE GATE_TIMEOUT -->|"empty"| ON_TIMEOUT GATE_TIMEOUT -->|"exception"| ON_SELECT_FAIL ON_SELECT_FAIL --> ON_RESPONSE TIMED_PROMPT -.->|"EOFError"| ON_EOF ON_RESPONSE -->|"Y/yes on update"| DISMISS_STATE ON_RESPONSE -->|"N/no on update"| DISMISS_STATE ON_TIMEOUT -->|"__timeout__ sentinel"| NO_WRITE(["No state change<br/>Prompt reappears"]) ON_RESPONSE -->|"cook success"| ONBOARDED ON_RESPONSE -->|"bypass phrase match"| STATE_YAML SIGNAL --> GATE_DISMISS INSTALL_INFO --> GATE_DISMISS DISMISS_STATE --> GATE_DISMISS GATE_DISMISS -->|"not dismissed"| TIMED_PROMPT GATE_DISMISS -->|"dismissed"| SKIP(["Skip prompt"]) ONBOARDED --> IS_FIRST_RUN IS_FIRST_RUN -->|"true"| TIMED_PROMPT IS_FIRST_RUN -->|"false"| SKIP GATE_SECRET --> SCAN_RESULT AST_SCAN -.->|"enforces"| TIMED_PROMPT BEHAV_TESTS -.->|"enforces"| GATE_TTY %% ═══════ CLASS ASSIGNMENTS ═══════ %% class START,FAIL_EXIT,NO_WRITE,SKIP terminal; class TTY_GUARD,TIMED_PROMPT,STATUS_LINE newComponent; class SIGNAL,INSTALL_INFO,SCAN_RESULT detector; class DISMISS_STATE,FETCH_CACHE,STATE_YAML stateNode; class ONBOARDED,SESSION_ID handler; class IS_FIRST_RUN,IS_DISMISSED phase; class GATE_TTY,GATE_TIMEOUT,GATE_SECRET,GATE_DISMISS detector; class ON_RESPONSE,ON_TIMEOUT,ON_EOF,ON_SELECT_FAIL output; class AST_SCAN,BEHAV_TESTS gap;Module Dependency Diagram
%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 70, 'curve': 'basis'}}}%% graph TB %% CLASS DEFINITIONS %% classDef cli fill:#1a237e,stroke:#7986cb,stroke-width:2px,color:#fff; classDef phase fill:#6a1b9a,stroke:#ba68c8,stroke-width:2px,color:#fff; classDef handler fill:#e65100,stroke:#ffb74d,stroke-width:2px,color:#fff; classDef stateNode fill:#004d40,stroke:#4db6ac,stroke-width:2px,color:#fff; classDef newComponent fill:#2e7d32,stroke:#81c784,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 output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff; subgraph L3_CLI ["L3 — CLI APPLICATION LAYER"] direction TB APP["● cli/app.py<br/>━━━━━━━━━━<br/>CLI entry point<br/>4 timed_prompt calls"] COOK["● cli/_cook.py<br/>━━━━━━━━━━<br/>Interactive session launcher<br/>1 timed_prompt call"] INIT["● cli/_init_helpers.py<br/>━━━━━━━━━━<br/>Init & registration<br/>4 timed_prompt calls"] ONBOARD["● cli/_onboarding.py<br/>━━━━━━━━━━<br/>First-run guided menu<br/>3 timed_prompt calls"] UPDATE["● cli/_update_checks.py<br/>━━━━━━━━━━<br/>Startup update signals<br/>1 status_line + 1 timed_prompt"] WKSP_CLI["● cli/_workspace.py<br/>━━━━━━━━━━<br/>Workspace clean helpers<br/>2 timed_prompt calls"] TIMED["★ cli/_timed_input.py<br/>━━━━━━━━━━<br/>timed_prompt + status_line<br/>Fan-in: 6 CLI modules"] ANSI["cli/_ansi.py<br/>━━━━━━━━━━<br/>supports_color, NO_COLOR"] TERMINAL["cli/_terminal.py<br/>━━━━━━━━━━<br/>terminal_guard TTY restore"] HOOKS_CLI["cli/_hooks.py<br/>━━━━━━━━━━<br/>Hook registration"] PROMPTS["cli/_prompts.py<br/>━━━━━━━━━━<br/>Orchestrator prompt builder"] end subgraph L1_INFRA ["L1 — INFRASTRUCTURE LAYER"] direction TB CONFIG["config/<br/>━━━━━━━━━━<br/>Settings + dynaconf"] EXEC["execution/<br/>━━━━━━━━━━<br/>Subprocess orchestration"] WKSP["workspace/<br/>━━━━━━━━━━<br/>Worktree + skill resolver"] PIPE["pipeline/<br/>━━━━━━━━━━<br/>Gate state + telemetry"] end subgraph L2_DOMAIN ["L2 — DOMAIN LAYER"] direction TB RECIPE["recipe/<br/>━━━━━━━━━━<br/>11 files import _type_constants<br/>Heaviest core consumer"] end subgraph L0_CORE ["L0 — FOUNDATION (zero internal imports)"] direction TB TYPES["● core/_type_constants.py<br/>━━━━━━━━━━<br/>Tool sets + PackDef<br/>Fan-in: 27 files"] CORE_PUB["core/__init__.py<br/>━━━━━━━━━━<br/>Re-export hub for L0"] CORE_IO["core/io.py<br/>━━━━━━━━━━<br/>atomic_write, YAML"] CORE_PATHS["core/paths.py<br/>━━━━━━━━━━<br/>pkg_root, is_git_worktree"] end subgraph HOOKREG ["MODULE — hook_registry"] direction TB HREG["hook_registry.py<br/>━━━━━━━━━━<br/>HookDef, HOOK_REGISTRY"] end subgraph TESTS_LAYER ["TESTS — Validation Layer"] direction TB T_TTY["● tests/cli/test_input_tty_contracts.py<br/>━━━━━━━━━━<br/>AST: enforces timed_prompt contract"] T_COOK["● tests/cli/test_cook.py<br/>━━━━━━━━━━<br/>Cook session tests"] T_ONBOARD["● tests/cli/test_onboarding.py<br/>━━━━━━━━━━<br/>Onboarding tests"] T_UPDATE["● tests/cli/test_update_checks.py<br/>━━━━━━━━━━<br/>Update check tests"] T_ARCH["● tests/arch/test_subpackage_isolation.py<br/>━━━━━━━━━━<br/>L0/L1/L2/L3 isolation"] T_RULES["● tests/arch/_rules.py<br/>━━━━━━━━━━<br/>Shared AST rule defs"] T_SIGTERM["● tests/execution/test_recording_sigterm.py<br/>━━━━━━━━━━<br/>SIGTERM signal handling"] T_SCHEMA["● tests/infra/test_schema_version_convention.py<br/>━━━━━━━━━━<br/>JSON write-site allowlist"] end subgraph EXTERNAL ["EXTERNAL PACKAGES"] direction LR HTTPX["httpx"] CYCLOPTS["cyclopts"] PACKAGING["packaging"] end %% === TIMED_INPUT FAN-IN (6 CLI consumers → new module) === %% APP -->|"imports timed_prompt"| TIMED COOK -->|"imports timed_prompt"| TIMED INIT -->|"imports timed_prompt"| TIMED ONBOARD -->|"imports timed_prompt"| TIMED UPDATE -->|"imports status_line + timed_prompt"| TIMED WKSP_CLI -->|"imports timed_prompt"| TIMED %% === TIMED_INPUT DEPENDENCIES (downward) === %% TIMED -->|"supports_color"| ANSI TIMED -->|"_require_interactive_stdin"| INIT %% === CLI INTERNAL WIRING === %% APP -->|"cook_interactive"| COOK APP -->|"helpers, CliError"| INIT APP -->|"terminal_guard"| TERMINAL APP -->|"run_update_checks"| UPDATE APP -->|"run_workspace_clean"| WKSP_CLI APP -->|"_build_orchestrator_prompt"| PROMPTS COOK -->|"mark_onboarded, is_first_run"| ONBOARD COOK -->|"terminal_guard"| TERMINAL ONBOARD -->|"_KNOWN_SCANNERS"| INIT UPDATE -->|"_claude_settings_path"| HOOKS_CLI UPDATE -->|"terminal_guard"| TERMINAL INIT -->|"CliError (deferred)"| APP %% === CLI → L1 DOWNWARD DEPENDENCIES === %% APP -->|"load_config"| CONFIG APP -->|"build_interactive_cmd"| EXEC APP -->|"DefaultSkillResolver"| WKSP COOK -->|"session skills"| WKSP COOK -->|"load_config"| CONFIG INIT -->|"write_config_layer"| CONFIG WKSP_CLI -->|"load_config, worktree ops"| CONFIG WKSP_CLI -->|"subprocess runner"| EXEC WKSP_CLI -->|"worktree + runs dirs"| WKSP %% === CLI → L0 DOWNWARD DEPENDENCIES === %% APP --> CORE_PUB COOK --> CORE_PUB INIT --> CORE_PUB ONBOARD --> CORE_PUB UPDATE --> CORE_PUB PROMPTS --> CORE_PUB %% === CLI → EXTERNAL === %% APP -->|"CLI framework"| CYCLOPTS UPDATE -->|"HTTP client"| HTTPX UPDATE -->|"version parsing"| PACKAGING %% === CLI → hook_registry === %% UPDATE -->|"_count_hook_registry_drift"| HREG %% === L0 INTERNAL === %% CORE_PUB -->|"re-exports all 16 names"| TYPES CORE_PUB --> CORE_IO CORE_PUB --> CORE_PATHS %% === _TYPE_CONSTANTS FAN-IN (cross-layer) === %% PIPE -->|"GATED_TOOLS, UNGATED_TOOLS, HEADLESS_TOOLS"| TYPES RECIPE -->|"SKILL_TOOLS + 5 tool sets (11 files)"| TYPES CONFIG -->|"PACK_REGISTRY, CATEGORY_TAGS"| TYPES EXEC -->|"CONTEXT_EXHAUSTION_MARKER"| TYPES WKSP -->|"PackDef, PACK_REGISTRY"| TYPES %% === L2 → L0 === %% RECIPE --> CORE_PUB %% === L1 → L0 === %% CONFIG --> CORE_PUB EXEC --> CORE_PUB WKSP --> CORE_PUB PIPE --> CORE_PUB %% === TEST DEPENDENCIES === %% T_TTY -.->|"AST validates"| TIMED T_TTY -.->|"AST scans"| INIT T_TTY -.->|"AST scans"| COOK T_TTY -.->|"AST scans"| WKSP_CLI T_COOK -.->|"tests"| COOK T_COOK -.->|"tests"| PROMPTS T_ONBOARD -.->|"tests"| ONBOARD T_UPDATE -.->|"tests"| UPDATE T_ARCH -.->|"enforces"| T_RULES T_ARCH -.->|"validates layer isolation"| L0_CORE %% === CLASS ASSIGNMENTS === %% class APP,COOK,INIT,ONBOARD,UPDATE,WKSP_CLI,ANSI,TERMINAL,HOOKS_CLI,PROMPTS cli; class TIMED newComponent; class CONFIG,EXEC,WKSP,PIPE handler; class RECIPE phase; class TYPES,CORE_PUB,CORE_IO,CORE_PATHS stateNode; class HREG handler; class T_TTY,T_COOK,T_ONBOARD,T_UPDATE,T_ARCH,T_RULES,T_SIGTERM,T_SCHEMA detector; class HTTPX,CYCLOPTS,PACKAGING integration;Closes #774
Implementation Plan
Plan file:
.autoskillit/temp/rectify/rectify_startup_update_freeze_2026-04-12_235000.md🤖 Generated with Claude Code via AutoSkillit
Token Usage Summary