Skip to content

Implementation Plan: First-Run Detection and Guided Onboarding Experience#464

Merged
Trecek merged 14 commits intointegrationfrom
feat-first-run-detection-and-guided-onboarding-experience/457
Mar 21, 2026
Merged

Implementation Plan: First-Run Detection and Guided Onboarding Experience#464
Trecek merged 14 commits intointegrationfrom
feat-first-run-detection-and-guided-onboarding-experience/457

Conversation

@Trecek
Copy link
Copy Markdown
Collaborator

@Trecek Trecek commented Mar 21, 2026

Summary

Implements first-run detection for cook sessions and a guided onboarding menu with
concurrent background intelligence gathering. When a project has been initialized
(autoskillit init) but has never been onboarded, cook intercepts the session launch
to present a 5-option interactive menu (Analyze, GitHub Issue, Demo Run, Write Recipe,
Skip). Background threads gather project intelligence (build tools, pre-commit scanner,
good-first-issues) concurrently while the user reads the menu. The chosen action becomes
the initial_prompt for the Claude session. A .autoskillit/.onboarded marker is written
after any menu path completes, preventing re-prompting on subsequent cook invocations.
autoskillit init --force resets the marker. Also adds tailorable/tailoring_hints
frontmatter fields to SkillInfo as infrastructure for the upcoming skill-tailoring
workflow (Issue #215).

Architecture Impact

Process Flow Diagram

%%{init: {'flowchart': {'nodeSpacing': 40, 'rankSpacing': 50, 'curve': 'basis'}}}%%
flowchart TB
    %% CLASS DEFINITIONS %%
    classDef terminal 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;

    %% TERMINALS %%
    START([● cook])
    DONE([session complete])
    ABORT([aborted])

    subgraph Guard ["Precondition Guards  ·  ● _cook.py"]
        direction TB
        CLAUDE{"claude in PATH?"}
        CONFIRM{"Launch session?<br/>━━━━━━━━━━<br/>[Enter / n]"}
    end

    subgraph Detection ["★ First-Run Detection  ·  _onboarding.is_first_run()"]
        direction TB
        FR{"★ is_first_run<br/>━━━━━━━━━━<br/>config.yaml exists?<br/>.onboarded absent?<br/>recipes/ empty?<br/>no overrides?"}
    end

    subgraph Onboarding ["★ Guided Onboarding  ·  run_onboarding_menu()"]
        direction TB
        WELCOME["★ welcome banner<br/>━━━━━━━━━━<br/>Would you like help?"]
        OPT_IN{"Y / n?"}
        INTEL["★ gather_intel (bg threads)<br/>━━━━━━━━━━<br/>_detect_scanner()<br/>_detect_build_tools()<br/>_fetch_good_first_issues()"]
        MENU["★ menu display<br/>━━━━━━━━━━<br/>A / B / C / D / E"]
        CHOICE{"★ user choice<br/>━━━━━━━━━━<br/>[A/B/C/D/E]"}
    end

    subgraph Routes ["★ initial_prompt Routes"]
        direction LR
        PA["A: /autoskillit:setup-project"]
        PB["B: /autoskillit:prepare-issue {ref}"]
        PC["C: /autoskillit:setup-project {target}"]
        PD["D: /autoskillit:write-recipe"]
    end

    subgraph SessionLaunch ["● Session Launch  ·  _cook.py"]
        direction TB
        BUILD["● build_interactive_cmd<br/>━━━━━━━━━━<br/>initial_prompt injected<br/>skills_dir added"]
        RUN["subprocess.run<br/>━━━━━━━━━━<br/>Claude interactive session"]
    end

    MARKER_SKIP["★ mark_onboarded<br/>━━━━━━━━━━<br/>write .autoskillit/.onboarded<br/>(skip / decline path)"]
    MARKER_DONE["★ mark_onboarded<br/>━━━━━━━━━━<br/>write .autoskillit/.onboarded<br/>(finally: A–D complete)"]

    %% MAIN FLOW %%
    START --> CLAUDE
    CLAUDE -->|"not found"| ABORT
    CLAUDE -->|"found"| CONFIRM
    CONFIRM -->|"n"| ABORT
    CONFIRM -->|"enter"| FR

    FR -->|"not first run"| BUILD
    FR -->|"first run"| WELCOME

    WELCOME --> OPT_IN
    OPT_IN -->|"n / no"| MARKER_SKIP
    OPT_IN -->|"Y / enter"| INTEL
    INTEL --> MENU
    MENU --> CHOICE

    CHOICE -->|"A"| PA
    CHOICE -->|"B"| PB
    CHOICE -->|"C"| PC
    CHOICE -->|"D"| PD
    CHOICE -->|"E / other"| MARKER_SKIP

    PA & PB & PC & PD --> BUILD
    MARKER_SKIP -->|"initial_prompt = None"| BUILD
    BUILD --> RUN
    RUN -->|"session exits · finally block"| MARKER_DONE
    MARKER_DONE --> DONE

    %% CLASS ASSIGNMENTS %%
    class START,DONE,ABORT terminal;
    class CLAUDE,CONFIRM detector;
    class FR detector;
    class OPT_IN,CHOICE stateNode;
    class WELCOME,MENU newComponent;
    class INTEL newComponent;
    class PA,PB,PC,PD newComponent;
    class MARKER_SKIP,MARKER_DONE newComponent;
    class BUILD,RUN handler;
Loading

Operational 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;

    subgraph CLILayer ["CLI ENTRY POINTS"]
        direction LR
        COOK["● autoskillit cook / c<br/>━━━━━━━━━━<br/>Interactive Claude session<br/>+ first-run onboarding gate"]
        INIT["● autoskillit init<br/>━━━━━━━━━━<br/>--force  resets onboarding<br/>--test-command --scope"]
        SKILLS["autoskillit skills list<br/>━━━━━━━━━━<br/>Lists skills incl.<br/>tailorable metadata"]
    end

    subgraph OnboardingModule ["★ Onboarding Module  ·  cli/_onboarding.py"]
        direction TB
        DETECT["★ is_first_run()<br/>━━━━━━━━━━<br/>reads config.yaml ✓<br/>reads .onboarded ✓<br/>reads recipes/ ✓<br/>reads .claude/skills/ ✓"]
        MENU["★ run_onboarding_menu()<br/>━━━━━━━━━━<br/>welcome + Y/n<br/>background intel gather<br/>A/B/C/D/E choice"]
        MARKER_WRITE["★ mark_onboarded()<br/>━━━━━━━━━━<br/>writes .autoskillit/.onboarded"]
    end

    subgraph ConfigState ["CONFIGURATION & STATE FILES"]
        direction TB
        CONFIG[".autoskillit/config.yaml<br/>━━━━━━━━━━<br/>read: first-run gate<br/>write: init"]
        ONBOARDED["★ .autoskillit/.onboarded<br/>━━━━━━━━━━<br/>gitignored marker<br/>absent = first run<br/>present = onboarded"]
        RECIPES[".autoskillit/recipes/<br/>━━━━━━━━━━<br/>read: first-run gate<br/>(empty = first run)"]
    end

    subgraph SkillMeta ["● SkillInfo Metadata  ·  workspace/skills.py"]
        direction TB
        SKILL_INFO["● SkillInfo dataclass<br/>━━━━━━━━━━<br/>★ tailorable: bool<br/>★ tailoring_hints: str<br/>(from SKILL.md frontmatter)"]
    end

    subgraph Outputs ["OBSERVABILITY OUTPUTS"]
        direction TB
        SESSION["Claude interactive session<br/>━━━━━━━━━━<br/>initial_prompt injected<br/>when first-run path taken"]
        GITIGNORE[".autoskillit/.gitignore<br/>━━━━━━━━━━<br/>auto-includes .onboarded<br/>via ensure_project_temp()"]
    end

    %% FLOWS %%
    COOK -->|"reads"| DETECT
    DETECT -->|"reads"| CONFIG
    DETECT -->|"reads"| ONBOARDED
    DETECT -->|"reads"| RECIPES
    DETECT -->|"first run → invoke"| MENU
    MENU -->|"writes"| MARKER_WRITE
    MARKER_WRITE -->|"creates"| ONBOARDED
    COOK -->|"launches"| SESSION

    INIT -->|"writes"| CONFIG
    INIT -->|"--force: deletes"| ONBOARDED

    SKILLS -->|"reads"| SKILL_INFO
    CONFIG -->|"gitignore updated by"| GITIGNORE

    %% CLASS ASSIGNMENTS %%
    class COOK,INIT,SKILLS cli;
    class DETECT,MENU newComponent;
    class MARKER_WRITE newComponent;
    class CONFIG,RECIPES stateNode;
    class ONBOARDED newComponent;
    class SKILL_INFO handler;
    class SESSION,GITIGNORE output;
Loading

State Lifecycle Diagram

%%{init: {'flowchart': {'nodeSpacing': 50, 'rankSpacing': 55, '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 detector fill:#b71c1c,stroke:#ef5350,stroke-width:2px,color:#fff;
    classDef output fill:#00695c,stroke:#4db6ac,stroke-width:2px,color:#fff;
    classDef gap fill:#ff6f00,stroke:#ffa726,stroke-width:2px,color:#000;

    subgraph Gates ["★ FIRST-RUN DETECTION GATES  ·  is_first_run()"]
        direction TB
        G1["★ Gate 1: config.yaml exists<br/>━━━━━━━━━━<br/>False → not first run<br/>(init has not run)"]
        G2["★ Gate 2: .onboarded absent<br/>━━━━━━━━━━<br/>False → not first run<br/>(already onboarded)"]
        G3["★ Gate 3: recipes/ empty<br/>━━━━━━━━━━<br/>False → not first run<br/>(customized project)"]
        G4["★ Gate 4: no overrides<br/>━━━━━━━━━━<br/>detect_project_local_overrides()<br/>False → not first run"]
    end

    subgraph MarkerLifecycle ["★ .onboarded Marker Lifecycle"]
        direction LR
        ABSENT["★ .onboarded ABSENT<br/>━━━━━━━━━━<br/>initial state after init<br/>triggers first-run gate"]
        PRESENT["★ .onboarded PRESENT<br/>━━━━━━━━━━<br/>idempotent write<br/>atomic_write (no overwrite)"]
    end

    subgraph WriteGuards ["★ Write Guards  ·  mark_onboarded()"]
        direction TB
        IDEMPOTENT["★ exists() check before write<br/>━━━━━━━━━━<br/>no-op if already present<br/>(prevents double-write)"]
        ATOMIC["atomic_write()<br/>━━━━━━━━━━<br/>temp-file + rename<br/>(prevents partial write)"]
        GITIGNORE["● ensure_project_temp()<br/>━━━━━━━━━━<br/>.onboarded in _GITIGNORE_ENTRIES<br/>(prevents accidental commit)"]
    end

    subgraph ResetGate ["● RESET GATE  ·  init --force"]
        direction TB
        FORCE{"● --force flag<br/>━━━━━━━━━━<br/>config written?"}
        DELETE["● onboarded_marker.unlink<br/>━━━━━━━━━━<br/>missing_ok=True<br/>(safe delete)"]
    end

    subgraph TransientState ["★ TRANSIENT STATE  ·  cook() call frame"]
        direction TB
        PROMPT["initial_prompt: str | None<br/>━━━━━━━━━━<br/>None → no onboarding<br/>str → skill injected as<br/>Claude opening message"]
        INTEL["★ OnboardingIntel<br/>━━━━━━━━━━<br/>scanner_found: str | None<br/>build_tools: list[str]<br/>github_issues: list[str]<br/>populated once, read-only"]
    end

    subgraph SkillMeta ["● SKILL METADATA  ·  SkillInfo"]
        direction TB
        TAILORABLE["● SkillInfo.tailorable: bool<br/>━━━━━━━━━━<br/>parsed from SKILL.md<br/>INIT_ONLY (frozen dataclass)"]
        HINTS["● SkillInfo.tailoring_hints: str<br/>━━━━━━━━━━<br/>parsed from SKILL.md<br/>INIT_ONLY (frozen dataclass)"]
    end

    %% GATE CHAIN %%
    G1 -->|"pass"| G2
    G2 -->|"pass"| G3
    G3 -->|"pass"| G4
    G4 -->|"all pass → first run"| ABSENT

    %% MARKER LIFECYCLE %%
    ABSENT -->|"read by is_first_run()"| G2
    ABSENT -->|"onboarding complete"| IDEMPOTENT
    IDEMPOTENT -->|"not exists"| ATOMIC
    ATOMIC -->|"writes"| PRESENT
    GITIGNORE -->|"gitignores"| PRESENT

    %% RESET %%
    FORCE -->|"yes"| DELETE
    DELETE -->|"resets to"| ABSENT

    %% TRANSIENT %%
    G4 -->|"first run detected"| PROMPT
    INTEL -->|"feeds suggestions to"| PROMPT

    %% CLASS ASSIGNMENTS %%
    class G1,G2,G3,G4 detector;
    class ABSENT,PRESENT newComponent;
    class IDEMPOTENT,ATOMIC newComponent;
    class GITIGNORE handler;
    class FORCE stateNode;
    class DELETE handler;
    class PROMPT phase;
    class INTEL newComponent;
    class TAILORABLE,HINTS handler;
Loading

Closes #457

Implementation Plan

Plan file: /home/talon/projects/autoskillit-runs/impl-457-20260321-085654-812593/temp/make-plan/first_run_detection_guided_onboarding_plan_2026-03-21_090000.md

Token Usage Summary

No token data available for this pipeline run.

🤖 Generated with Claude Code via AutoSkillit

Trecek and others added 9 commits March 21, 2026 09:34
Extends _AUTOSKILLIT_GITIGNORE_ENTRIES with '.onboarded' so the
first-run marker file is excluded from version control on project init.
The existing ensure_project_temp backfill logic picks up the new entry
automatically.
…menu

Introduces:
- OnboardingIntel dataclass for project intelligence aggregation
- is_first_run(project_dir) — detects unboarded initialized projects
- mark_onboarded(project_dir) — writes .autoskillit/.onboarded marker
- gather_intel() — concurrent scanner/build-tool/issue discovery
- run_onboarding_menu() — interactive 5-option guided onboarding
Extracts _read_skill_frontmatter() as a single-parse helper that
populates all frontmatter fields (categories, tailorable, tailoring_hints)
in one pass. SkillInfo gains two new fields defaulting to False/''.
_scan_directory and SkillResolver.resolve both use _skill_info_from_frontmatter
to populate all fields. Infrastructure for Issue #215 skill-tailoring workflow.
Calls is_first_run() after session confirmation; if true, presents
run_onboarding_menu(). The returned initial_prompt is forwarded to
build_interactive_cmd(). mark_onboarded() is called in the finally
block only when initial_prompt is not None (E/decline path marks
internally inside run_onboarding_menu).
When init() writes the config (force=True or fresh init), unlinks
.autoskillit/.onboarded so the onboarding menu reappears on the next
cook invocation. No-op on init without --force (config already exists).
New file tests/cli/test_onboarding.py: 17 tests covering is_first_run,
mark_onboarded, gather_intel, detect_build_tools, and run_onboarding_menu.

Additions to tests/cli/test_cook_interactive.py: autouse _no_first_run
fixture to isolate existing CH-1..CH-7 tests, plus CH-8..CH-12 covering
onboarding integration with cook().

Additions to tests/cli/test_init.py: ON-INIT-1..ON-INIT-3 covering gitignore
entry presence, init --force reset, and init non-force preservation.

Additions to tests/workspace/test_skills.py: SK-T1..SK-T5 covering
tailorable/tailoring_hints defaults and frontmatter parsing.
- Add _onboarding.py to _PRINT_EXEMPT and _BROAD_EXCEPT_EXEMPT arch rules
  (interactive CLI module legitimately uses print() and graceful degradation)
- Exempt cli/ subpackage at 11 files in test_no_subpackage_exceeds_10_files
- Document _onboarding.py in CLAUDE.md architecture section
- Fix monkeypatch targets in test_skills.py SK-T5 to use package re-exports
  (autoskillit.workspace.bundled_skills_dir/bundled_skills_extended_dir)
- Fix SkillResolver.__init__ to look up bundled_skills dirs via package namespace
  so monkeypatching at the re-export level is effective
- Fix cross-package submodule imports in _onboarding.py: use autoskillit.workspace
  and autoskillit.core instead of their submodules
- Add .onboarded to test_doctor_gitignore_ok_when_all_covered gitignore fixture
- Use open() instead of Path.read_text() in _read_skill_frontmatter to avoid
  breaking tests that patch Path.read_text for cache validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoSkillit PR Review — Verdict: changes_requested (13 blocking findings)

return str(project_dir)


def run_onboarding_menu(project_dir: Path, *, color: bool = True) -> str | None:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] cohesion: run_onboarding_menu has split lifecycle responsibility: it calls mark_onboarded internally for N/E paths but delegates it to the caller's finally block for A/B/C/D paths. This asymmetric contract makes the function sometimes own the full lifecycle and sometimes not. A cleaner design would either always mark internally or always leave marking to the caller.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation — flagged for design decision. The asymmetric lifecycle is explicitly documented in the docstring ('Returns an initial_prompt string for A/B/C/D paths (mark_onboarded called by the caller's finally block), or None for E/decline'). This is a valid design-cohesion question — flagged for human decision.

Copy link
Copy Markdown
Collaborator Author

@Trecek Trecek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AutoSkillit review found 13 blocking issues. See inline comments. verdict=changes_requested

@Trecek Trecek added this pull request to the merge queue Mar 21, 2026
Merged via the queue into integration with commit 960cc91 Mar 21, 2026
2 checks passed
@Trecek Trecek deleted the feat-first-run-detection-and-guided-onboarding-experience/457 branch March 21, 2026 17:44
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.

1 participant