From lazycortex-core
Bootstrap the lazycortex-core plugin for the current project (or globally). Copies every rule template shipped by the plugin into the rules directory, syncs authoring templates into `.claude/templates/core/`, bootstraps the scaffold registry, seeds runtime defaults, and offers expert wizard and daemon supervisor setup. Idempotent — safe to re-run. Detects install scope automatically.
How this skill is triggered — by the user, by Claude, or both
Slash command
/lazycortex-core:lazy-core.installThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Bootstrap the plugin in the right scope: copy every rule template shipped by the plugin into the target `rules/` directory, sync authoring templates into the consumer's `templates/core/` directory, and ensure the scaffold registry is in place.
Bootstrap the plugin in the right scope: copy every rule template shipped by the plugin into the target rules/ directory, sync authoring templates into the consumer's templates/core/ directory, and ensure the scaffold registry is in place.
This skill has 16 ordered steps. The executing agent MUST NOT skip, merge, reorder, or silently omit any step. To make dropped steps structurally impossible:
TaskCreate with exactly one task per step below — no merging, no abbreviation, no renaming. The canonical list (use these titles verbatim):
Step 0 — Verify Python ≥ 3.12 (floor)Step 1 — Detect install scopeStep 2 — Determine pathsStep 3 — Sync rule templatesStep 4 — Sync authoring templatesStep 5 — VerifyStep 6 — Seed lazy.settings.jsonStep 7 — Bootstrap .logs/, .runtime/, lazy.settings.local.json gitignore, and .lazyignoreStep 8 — Migrate stale lazycortex-log hook registrationsStep 9 — Bootstrap runtime defaultsStep 10 — Bootstrap experts directoryStep 10.5 — Bootstrap .memory/ directoryStep 11 — Expert-add wizardStep 12 — Bootstrap expert-pump routineStep 13 — Offer daemon supervisor installStep 13.5 — Document expert-spawn sandbox in settings.local.jsonStep 14 — ReportStep 15 — Log the runin_progress on enter and completed on exit. "Completed" means "I executed the step's logic AND produced a report line for it". No-ops count only if they produced an explicit outcome line (e.g. asserted, already-ignored, absent, skipped-per-user-choice).TaskList shows every prior task completed or explicitly skipped with an outcome. A still-pending task is a bug — stop and execute it first.Every plugin in this marketplace requires Python ≥ 3.12. This step runs first; on a machine that already meets the floor it is silent (one python3 -V invocation) and the install proceeds straight to Step 1. Per-plugin <ns>.install skills inherit this gate — they do NOT re-probe.
Run Bash(python3 -V) and parse the version. If python3 is missing or the version is below 3.12.0, walk the user through install via AskUserQuestion:
AskUserQuestion:
question: "Python 3.12+ is required (found `<detected version or 'not found'>`). How would you like to install it?"
description: "All LazyCortex plugins target Python 3.12 as a single floor. Pick the route that matches this machine; once Python is upgraded, re-run `/lazy-core.install`."
options:
- "macOS — `brew install [email protected] && brew link [email protected] --force`"
- "Linux / cross-platform — `pyenv install 3.12 && pyenv global 3.12`"
- "Skip — abort install"
On macOS / Linux options: print the corresponding command for the user to run in their own shell — do NOT execute it (this skill never installs system packages on the user's behalf). Then state outcome awaiting-user-install and abort the run; the user re-runs /lazy-core.install once the upgrade lands.
On Skip — abort install: state outcome aborted-python-floor-not-met and exit with the message Python 3.12+ required — re-run /lazy-core.install once installed.. Skip Steps 1–15.
If python3 -V reports ≥ 3.12.0: state outcome python-floor-ok (<version>) and proceed to Step 1.
When raising the floor in the future, bump this step's numeric threshold in the same edit as any other floor-bearing reference.
Read ~/.claude/plugins/installed_plugins.json. The lazycortex-core@lazycortex key holds an array of entries — one per project where /plugin install was last run. The plugin cache is shared globally across all projects, so any non-empty array proves the plugin is installed and usable in the current cwd.
Do NOT compare an entry's projectPath against the current working directory. projectPath records where the install command was last run, not where the plugin "belongs" — Step 2 of this skill targets <repo-root> (i.e. git rev-parse --show-toplevel in the current cwd) regardless of any entry's projectPath. A projectPath mismatch is never grounds for aborting.
Look at the scope field of the entries in the array:
"user" — plugin enabled globally in ~/.claude/settings.json"project" — plugin enabled per-project in .claude/settings.jsonIf both scopes appear in the array, ask the user which to target. Default: project.
Abort only if the lazycortex-core@lazycortex key is absent or its array is empty — i.e. the plugin has never been installed on this machine. In that case tell the user to install it first:
"enabledPlugins": { "lazycortex-core@lazycortex": true }
then run /plugin install lazycortex/lazycortex-core.
Enumerate every rule file shipped by the plugin via Glob: <installPath>/rules/*.md — never hardcode filenames. <installPath> is the installPath field from installed_plugins.json.
For each source file <installPath>/rules/<name>.md, the target is:
| Scope | Rule destination | Templates destination |
|---|---|---|
user | ~/.claude/rules/<name>.md | ~/.claude/templates/core/ |
project | <repo-root>/.claude/rules/<name>.md | <repo-root>/.claude/templates/core/ |
Project root is git rev-parse --show-toplevel (or current working directory if not in a git repo — warn the user).
If the glob returns zero files, abort and tell the user the plugin cache is empty — they likely need to run /plugin update lazycortex-core@lazycortex first.
Rules eat context on every session — the user owns the decision to install each one.
Glob <installPath>/rules/*.md.lazycortex- prefix (so lazycortex-core → lazy-core), plus every unique <ns>. prefix appearing in source rule filenames (for this plugin that includes both lazy-core and lazy-guard).Glob <targetRulesDir>/<ns>.*.md for each owned namespace. Union them.mkdir -p.Every per-rule prompt MUST surface the rule's purpose so the user (who may not remember what a given rule file does) can make an informed decision. Extract description: from the rule file's frontmatter — from the source file for New/Drift, from the target file for Orphan (source is gone). If the description is longer than ~200 chars, use its first sentence. If no description: field exists, fall back to the first non-heading line of the body, and flag the missing-description as a WARN in the report.
For every rule name in (source ∪ target), determine its state and act:
New — target missing, source present → AskUserQuestion with:
Install rule `<name>.md`?**Purpose:** <source description>\n\n**What this does:** Copies the shipped rule into `<targetPath>`. Rules are auto-loaded into every Claude Code session (when `always_loaded`) or when editing files matching their `paths:` scope.Unchanged — both present, byte-identical → no prompt. State unchanged.
Drift — both present, differ → AskUserQuestion with:
Rule `<name>.md` has drift — how to reconcile?AskUserQuestion ("Add <chunk-title> from the shipped version?") only when the chunk's intent is non-obvious; uncontroversial additions (e.g. new entries in a registry group that already exists locally with the same key) land without a prompt. Every local-only chunk stays untouched. Apply accepted chunks via Edit. State merged if any chunk landed, kept-local if zero (shipped contributed nothing the local file lacked).Heuristic for picking the default in the description text: when the local file's body or description: frontmatter declares itself a companion / project-scope extension / dev-vault variant of the shipped file (phrases like "companion to", "project-scope", "extends the global", or a registry group with a name different from the shipped group), the agent presents merge-shipped as (Recommended) and explains why in the description. When the local file looks like an unintentional stale copy, the agent surfaces overwrite as (Recommended) instead.
Orphan — target present, source missing → AskUserQuestion with:
Rule `<name>.md` is no longer shipped by the plugin — delete from `<targetDir>`?**Purpose (from your local copy):** <target description>\n\n**Why you're seeing this:** The plugin used to ship this rule but no longer does (renamed, merged into another rule, or deprecated). Keeping it means it stays loaded into your sessions but will never receive updates.rm <target>, state deleted. Keep → state kept-orphan.One AskUserQuestion at a time — wait for the answer before the next prompt.
Orphan detection only considers target files whose filename starts with one of this plugin's owned namespaces. Rules from other plugins and user-authored rules in unrelated namespaces are never offered for deletion.
lazy-core.scaffold.md — registry-block exemption (§5a)lazy-core.scaffold.md is special: its ## Registry fenced block is primitive-owned — written only by lazycortex-core scaffold via Step 4's scaffold-sync. When this file reaches the per-rule decision:
{} registry); Step 4 then populates it.overwrite. Reconcile only the prose/frontmatter region above ## Registry (merge-shipped, which never deletes local content); leave the ## Registry block byte-for-byte. The shipped block is {}, so it contributes nothing to merge.## Registry block here — surgical per-key registry writes are scaffold-sync's job.Authoring-template copy and scaffold-registry population are both done by lazy-core.scaffold-sync, invoked for lazycortex-core itself — core registers through the same path as any other plugin (dogfood).
Resolve this plugin's own <installPath> (the installPath field of lazycortex-core@lazycortex in installed_plugins.json) and the detected <scope> (project / user), then dispatch:
Skill(skill: "lazycortex-core:lazy-core.scaffold-sync", args: "plugin=lazycortex-core installPath=<installPath> scope=<scope>")
The skill discovers <installPath>/templates/core/scaffold.entries.json, copies templates/core/* (excluding the manifest) into <consumerScope>/.claude/templates/core/ with the same merge / overwrite / keep-local drift handling that previously lived here, and upserts the lazycortex-core registry key from the manifest via scaffold upsert (surgical — the consumer's _local and any sibling-plugin keys stay byte-for-byte; per §5a the rest of lazy-core.scaffold.md is untouched).
The scaffold-sync report: per-template copy states plus the registry upsert status.
For each installed rule file:
--- frontmatter parseslazy-core.doctor rule-size threshold)Non-destructively seed the agent_models section with the three built-in subagents and create empty reserved slots for user- and project-authored agents.
| Scope | Path |
|---|---|
user | ~/.claude/lazy.settings.json |
project | <repo-root>/.claude/lazy.settings.json |
Read the target file. If missing or unparseable, treat its contents as {"version": 1, "agent_models": {}}.
Ensure agent_models._builtin, agent_models._user, and agent_models._project exist as objects (create empty {} if absent — never overwrite existing content).
_builtin defaultsPull tier values from ${CLAUDE_PLUGIN_ROOT}/skills/lazy-core.agent-models/default-tiers.json — single source of truth for both this seed step and the lazy-core.agent-models wizard. Select every entry under defaults whose key matches the built-in dispatch set {Explore, Plan, general-purpose, statusline-setup} (bare names with no :). Those are the entries to seed under _builtin, key + tier verbatim from the JSON.
If default-tiers.json is missing or unparseable → FAIL with default-tiers.json missing or invalid at <path>; reinstall lazycortex-core. Don't fall back to hardcoded values — silent drift between this seed and the wizard's "accept all template defaults" batch is exactly what the SOT is meant to prevent.
Per-key semantics (write back only if anything changed):
agent_models._builtin → add the entry with the JSON's tier. State added.Never touch _user or _project entries — those slots are filled interactively by lazy-core.agent-models.
Before calling Write on a newly-created file (target was missing or unparseable), print this explanation in the conversation so the subsequent permission prompt has context above it:
Creating
<targetPath>at scope (user=~/.claude/lazy.settings.jsonapplies to every project;project=<repo-root>/.claude/lazy.settings.jsonapplies to this repo only).This file routes subagent dispatches to model tiers (
haiku/sonnet/opus/default). Structure:
_builtin— defaults for the three built-in subagent types (seeded now)._user— your globally-authored agents (filled later by/lazy-core.agent-models, writes to the global file)._project— this project's agents (filled later by/lazy-core.agent-models, writes to the project file).Routing rule:
/lazy-core.agent-modelsauto-routes by group —_user.*→ global file,_project.*→ project file, plugin-domain groups → the plugin's own install scope. Override with--scope=project|globalfor deliberate deviations.Scope precedence when both files exist: reads merge with project wins per-group — a duplicate group in the project file shadows the global file's copy.
The file looks mostly empty because
_user/_projectare reserved slots waiting for/lazy-core.agent-modelsto populate them based on the agents you actually have.
For existing files (mutations to an already-present file), print a one-line context instead: Updating <targetPath>: <N> _builtin default(s) added. No permission prompt is expected for in-place edits the user already owns, but the context line keeps the report grounded.
If any mutation happened, write the file with version: 1 at the top. Preserve existing groups (plugin-domain groups like lazycortex, third-party groups, etc.) verbatim.
One line per seeded default: _builtin.<key> = <value> (<state>). Plus _user, _project: created (empty) if new, unchanged otherwise.
Create .logs/ and .runtime/ at the repo root, ensure .gitignore covers both, ensure .gitignore also lists .claude/lazy.settings.local.json (the gitignored personal overlay companion to the tracked lazy.settings.json), and seed a default .lazyignore at the repo root when one is absent.
.logs/ — gitignored runtime journal (daemon output, recall logs, commit-recorder feed)..runtime/ — gitignored non-log daemon state (currently state.json carrying last_run / git_watch / daemon_halted)..claude/lazy.settings.local.json — gitignored personal-overlay file that lazy_settings.load_section deep-merges onto the tracked lazy.settings.json. No directory is created — the file is opt-in and materializes only when the consumer adds a local override. The .gitignore slot is reserved so accidental commits are impossible..lazyignore — tracked git excludes file carrying the extra excludes (on top of .gitignore) that every tree-walking routine honours via git's ignore engine: venvs, node_modules, __pycache__, in-tree worktrees. Seeded from the shipped template only when absent — the consumer's own copy is authoritative and never overwritten.All four concerns are handled by three helpers. This step runs unconditionally (not gated on runtime-setup confirmation); the .logs/ half is absorbed from the retired lazy-log.install skill.
Run via:
Bash(PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from pathlib import Path
from lazy_install_phases import bootstrap_logs_dir, bootstrap_lazy_settings_local_gitignore, bootstrap_lazyignore
print('.logs/+.runtime/:', bootstrap_logs_dir(Path('.')))
print('lazy.settings.local.json:', bootstrap_lazy_settings_local_gitignore(Path('.')))
print('.lazyignore:', bootstrap_lazyignore(Path('.'), Path('${CLAUDE_PLUGIN_ROOT}/templates/.lazyignore')))
")
Outcome per helper: bootstrapped (something was created/appended) or already-present for the first two; seeded / already-present / template-missing for .lazyignore.
The lazycortex-log plugin was retired and folded into lazycortex-core. Its hooks/lazy-log.commit-recorder.py was registered under ${CLAUDE_PLUGIN_ROOT}/lazycortex-log/hooks/ in consumer settings.json files. This step strips those stale registrations from the four standard settings paths so the retired plugin path no longer appears in the consumer's hook pipeline.
Run via:
Bash(PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from pathlib import Path
from lazy_install_phases import migrate_log_hooks
for p in [Path('.claude/settings.json'),
Path('.claude/settings.local.json'),
Path.home() / '.claude/settings.json',
Path.home() / '.claude/settings.local.json']:
print(f'{p}: {migrate_log_hooks(p)}')
")
Idempotent: a second run on already-clean files is a no-op. Report one line per path with its outcome word (migrated or no-stale-entries).
Steps 9–13 set up the per-repo runtime layer (.experts/, expert wizard, daemon supervisor). They operate on the current working repo, independent of the plugin's install scope — runtime artifacts are always per-repo, even when the plugin is installed at user scope.
For Steps 9–13, <repo-root> is the cwd's git toplevel (resolved or initialized in 9a below), even if Step 1 detected install scope as user.
Run git rev-parse --show-toplevel in cwd:
If it succeeds, set <repo-root> to the returned path and proceed to 9b.
If it fails (cwd is not inside a git repo), ask:
question: "Current directory is not a git repository. Initialize one here to enable runtime/experts setup?"
description: "Runtime artifacts (`.experts/`, daemon supervisor units) need a repo root. Initializing here means git-tracking your runtime config alongside the rest of the directory; skipping bypasses runtime/experts setup for this run — Steps 3–8 are unaffected."
options: ["Initialize git here", "Skip — no runtime setup"]
Initialize git here: run Bash(git init) in cwd, then set <repo-root> to cwd. Proceed to 9b.Skip — no runtime setup: mark Steps 9–13 with outcome skipped-not-in-git-repo, skip to Step 14.Ask once:
AskUserQuestion:
question: "Bootstrap runtime/experts for this repo at `<repo-root>`?"
description: "Sets up `lazy.settings.json[experts]`, `.claude/bin/lazy.runtime.sh` shim, the flat `daemon` + `routines` sections in `.claude/lazy.settings.json`, plus the expert-add wizard and optional daemon supervisor. Skip if you don't need this in this repo yet — the rest of the install is unaffected and you can re-run `/lazy-core.install` later."
options: ["Yes", "Skip — this repo doesn't need runtime/experts"]
Skip — this repo doesn't need runtime/experts: mark Steps 9–13 with outcome skipped-per-user-choice, skip to Step 14.Yes: continue with 9c below and run Steps 10–13.daemon + routines sectionsThe runtime daemon reads its config from flat top-level section keys — runtime_daemon.py calls load_section(path, "daemon") and load_section(path, "routines") directly, and expert_runtime.register_routine writes the flat routines section. Seed those two sections (never a nested lazy-core.runtime object — nothing reads that shape).
Read <repo-root>/.claude/lazy.settings.json. Seed each section only when its top-level key is absent (do NOT overwrite an existing section):
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
import json
from lazy_settings import save_section
from pathlib import Path
p = Path('<repo-root>/.claude/lazy.settings.json')
raw = json.loads(p.read_text()) if p.exists() else {}
if 'daemon' not in raw:
save_section(p, 'daemon', {
'git': None,
'polling_interval_sec': 5,
'cleanup_completed_after': '7d',
'cleanup_failed_after': '30d',
'cleanup_dead_after': '7d'
})
print('daemon: bootstrapped')
else:
print('daemon: already-present')
raw = json.loads(p.read_text()) if p.exists() else {}
if 'routines' not in raw:
save_section(p, 'routines', {})
print('routines: bootstrapped')
else:
print('routines: already-present')
"
State bootstrapped if either section was absent and was written; already-present if both already existed (do NOT overwrite); skipped-not-in-git-repo or skipped-per-user-choice if 9a/9b chose to skip.
If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, perform the following three idempotent operations:
lazy.runtime.sh shimThe shim is content-tracked so consumers pick up new shim features (e.g. the --dev-mode flag added in lazy-core 0.18) on re-install without manual cleanup.
Bash(mkdir -p <repo-root>/.claude/bin/)<repo-root>/.claude/bin/lazy.runtime.sh is absent → copy + chmod, state created.Bash(diff -q ${CLAUDE_PLUGIN_ROOT}/templates/runtime/lazy.runtime.sh <repo-root>/.claude/bin/lazy.runtime.sh) reports differences → copy + chmod, state refreshed.Copy command in cases 2 and 3:
Bash(cp ${CLAUDE_PLUGIN_ROOT}/templates/runtime/lazy.runtime.sh <repo-root>/.claude/bin/lazy.runtime.sh)
Bash(chmod +x <repo-root>/.claude/bin/lazy.runtime.sh)
The shim resolves the latest lazycortex-core/bin/runner from the plugin cache at exec time, so supervisor units don't need re-rendering after /plugin update. Re-copying on content drift is safe — the shim's interface is stable (positional repo-root + repeatable --plugin-dir; the new --dev-mode flag is additive).
lazy.settings.json[experts]Check whether the experts section exists in <repo-root>/.claude/lazy.settings.json. If missing, write it with content {"_version": 1} via lazy_settings.save_section.
State created if written; already-present if it existed.
.gitignore entriesRead <repo-root>/.gitignore (or treat as empty if missing). Ensure it contains the following line:
.experts/.logs/ and .runtime/ are owned by Step 7's bootstrap_logs_dir helper and need no entry here. The whole .experts/ tree is runtime scratch (job queue, cross-repo trackers, subprocess locks) — ignore the directory, not just .experts/.jobs/. If a legacy narrower .experts/.jobs/ line is present, replace it with .experts/; if no .experts/ line is present, append it with Edit (or Write if the file was missing). State updated if appended or replaced; already-present if .experts/ was already there.
If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, ensure .memory/ exists at the repo root and strip any legacy !.memory/ line from .gitignore (older versions of this skill wrote a defensive un-ignore line; the line was selective paranoia and is now retired — memory notes track in git the normal way):
Bash(PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from pathlib import Path
from lazy_install_phases import bootstrap_memory_dir
print(bootstrap_memory_dir(Path('.')))
")
Outcome: bootstrapped (dir created and/or legacy line stripped) or already-present (dir existed and no legacy line present).
If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, ask the user once:
AskUserQuestion:
question: "Scan installed plugins for expert candidates to register in this repo?"
options: ["Yes", "Skip — I'll do this later"]
On Skip — I'll do this later, state skipped-per-user-choice and move to Step 12.
On Yes, run the wizard:
Glob for agent files containing expert_protocol: frontmatter at three scopes. For the plugin cache, resolve the latest version per plugin via lexicographic sort on the version directory:
~/.claude/plugins/cache/*/*/ — list subdirectories, take the lexicographically last one as latest version, then glob <latest-version>/agents/*.md~/.claude/agents/*.md<repo-root>/.claude/agents/*.mdFor each candidate file, Read its frontmatter. If expert_protocol: is present, record:
source_scope: plugin-cache, user, or projectplugin (from the cache directory structure, or user/project for the latter two scopes)agent_name: the basename of the file without .mdexpert_protocol_ref: the value of expert_protocol: (a protocol reference string)Resolve the protocol file via:
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from reference_resolver import resolve_reference
from pathlib import Path
ref = '<expert_protocol_ref>'
resolved = resolve_reference(ref, base_path=Path('<repo-root>'))
print(resolved)
"
If resolve_reference raises an exception or returns None, skip that candidate and record it in the report as protocol-unresolvable.
If no candidates are found after scanning all three scopes, state no-candidates and proceed to Step 12.
Load the experts section of <repo-root>/.claude/lazy.settings.json (via lazy_settings.load_section). Skip any candidate whose agent_name already appears as a key in the section (besides _version).
If all candidates are filtered out, state all-already-registered and proceed to Step 12.
For each remaining candidate, ask:
AskUserQuestion:
question: "Install expert candidate `<plugin>:<agent_name>` (protocol `<expert_protocol_ref>`)?"
options: ["Yes", "Skip", "Stop wizard"]
On Skip: move to the next candidate. On Stop wizard: stop iterating; proceed to Step 12 with whatever was accepted so far. On Yes: ask for the three fields below (one AskUserQuestion each, strictly in sequence):
a. Local name for this expert in the project:
AskUserQuestion:
question: "Local name for this expert in the project?"
description: "Default: <agent_name>"
options: ["<agent_name> (default)", "Enter custom name"]
If <agent_name> (default) is chosen, use agent_name as the local name. If Enter custom name, prompt once more with a free-text question for the name.
b. Git author name:
AskUserQuestion:
question: "git_author.name for commits this expert makes?"
description: "Default: <output of `git config user.name`>"
options: ["<git config user.name> (default)", "Enter custom name"]
Use default if chosen; otherwise prompt once more.
c. Git author email:
AskUserQuestion:
question: "git_author.email for commits this expert makes?"
description: "Default: <output of `git config user.email`>"
options: ["<git config user.email> (default)", "Enter custom email"]
Use default if chosen; otherwise prompt once more.
lazy.settings.json[experts]For each accepted candidate, merge the new entry via:
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from pathlib import Path
from lazy_settings import load_section, save_section
p = Path('<repo-root>/.claude/lazy.settings.json')
section = load_section(p, 'experts')
section['<local_name>'] = {
'agent': '<plugin>:<agent_name>',
'git_author': {'name': '<author_name>', 'email': '<author_email>'}
}
save_section(p, 'experts', section)
"
State one line per candidate: <local_name>: registered or skipped.
If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, check two conditions:
experts section of <repo-root>/.claude/lazy.settings.json contains at least one expert entry (a key that is not _version and whose value is a dict).routines section of <repo-root>/.claude/lazy.settings.json (load_section(path, 'routines') — the same section register_routine writes to) does NOT already contain a key lazy-expert.pump.If both conditions are true, run:
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from expert_runtime import bootstrap_default_routines
from pathlib import Path
bootstrap_default_routines(Path('<repo-root>'))
"
State registered if the routine was added; already-present if it was already there; skipped-no-experts if condition 1 was false.
If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, only proceed if Step 12 produced outcome registered (i.e., the expert-pump routine was freshly added — there is something to drain). If Step 12 was already-present or skipped-no-experts, state skipped-no-pump and move on.
Ask:
AskUserQuestion:
question: "Install a daemon supervisor to run the expert-pump routine automatically?"
options: ["macOS launchd", "Linux systemd", "Skip — I'll start the daemon manually"]
On Skip — I'll start the daemon manually: state skipped-per-user-choice.
Before rendering the supervisor unit, decide whether to pass --dev-mode to lazy.runtime.sh. In dev-mode the shim scans <repo-root>/claude/*/.claude-plugin/plugin.json and prefers those plugin sources over the cache — useful when this repo IS the plugin's authoring vault.
Read the persisted choice via (it lives under the flat daemon section as daemon.supervisor.dev_mode — dev_mode is install-skill state with no runtime reader, so it rides inside the flat daemon section the daemon already loads, not a separate nested object):
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from lazy_settings import load_section
from pathlib import Path
sec = load_section(Path('<repo-root>/.claude/lazy.settings.json'), 'daemon')
print((sec.get('supervisor') or {}).get('dev_mode', 'unset'))
"
True or False → use that value, skip the question.unset → ask:AskUserQuestion:
question: "Run the supervisor in dev-mode?"
description: "Dev-mode prefers plugin sources under `<repo-root>/claude/*/` over the plugin cache. Pick `No` unless this repo is a LazyCortex plugin-authoring vault."
options: ["No (Recommended)", "Yes — prefer in-repo plugin sources"]
Persist the chosen dev_mode (boolean) under the flat daemon section at daemon.supervisor.dev_mode:
PYTHONPATH=${CLAUDE_PLUGIN_ROOT}/bin python3 -c "
from lazy_settings import load_section, save_section
from pathlib import Path
p = Path('<repo-root>/.claude/lazy.settings.json')
sec = load_section(p, 'daemon')
sec.setdefault('supervisor', {})['dev_mode'] = <True|False>
save_section(p, 'daemon', sec)
"
Hold the chosen boolean as <dev_mode> for 13b/13c.
On macOS launchd:
${CLAUDE_PLUGIN_ROOT}/templates/runtime/com.lazycortex.runtime.plist.{REPO_ROOT} → absolute path of <repo-root>, {REPO_NAME} → basename of <repo-root> (the shim path is built into the templates as {REPO_ROOT}/.claude/bin/lazy.runtime.sh — no separate runner-path substitution needed).<dev_mode> is True: insert a <string>--dev-mode</string> line into ProgramArguments between the lazy.runtime.sh line and the {REPO_ROOT} line. Indent matches the surrounding <string> lines (8 spaces).Bash(mkdir -p ~/Library/LaunchAgents/)~/Library/LaunchAgents/com.lazycortex.runtime.<REPO_NAME>.plist.Bash(launchctl load ~/Library/LaunchAgents/com.lazycortex.runtime.<REPO_NAME>.plist)<dev_mode> is True).On Linux systemd:
${CLAUDE_PLUGIN_ROOT}/templates/runtime/lazy-core-runtime.service.{REPO_ROOT} and {REPO_NAME} as above.<dev_mode> is True: replace lazy.runtime.sh {REPO_ROOT} (after step 2's substitution) with lazy.runtime.sh --dev-mode {REPO_ROOT} in the ExecStart= line.Bash(mkdir -p ~/.config/systemd/user/)~/.config/systemd/user/lazy-core-runtime-<REPO_NAME>.service.Bash(systemctl --user enable --now lazy-core-runtime-<REPO_NAME>.service)<dev_mode> is True).If Step 9 was skipped (outcome skipped-not-in-git-repo or skipped-per-user-choice), inherit the same outcome and skip this step.
Otherwise, the runtime daemon (Step 12+) spawns claude -p --permission-mode dontAsk subprocesses for every expert job. Without an explicit <repo-root>/.claude/settings.local.json sandbox + permission block, those spawns either run unrestricted (any earlier bypassPermissions left over) or are completely deny-by-default (dontAsk with no permissions.allow) and cannot Read/Write/Edit anything. Neither is correct.
This step does NOT write the file — settings.local.json is per-machine state the operator owns. Instead it (a) shows the recommended block and (b) Edits the file in place ONLY at the operator's explicit go-ahead, MERGING into existing keys rather than overwriting.
AskUserQuestion:
question: "Configure expert-spawn sandbox + permissions in `<repo-root>/.claude/settings.local.json`?"
description: "The runtime daemon will spawn `claude -p --permission-mode dontAsk` for every expert job. Without sandbox+permissions in settings.local.json those spawns can't Read/Write/Edit (or can read your entire home dir). settings.local.json is per-machine (gitignored). The skill will MERGE — never overwrite — your existing keys."
options: ["Yes — merge the recommended block", "Skip — I'll configure manually"]
On Skip — I'll configure manually: state outcome skipped-per-user-choice. Print the recommended block (below) for reference, then move to Step 14.
Show the operator the full block they should have under <repo-root>/.claude/settings.local.json. Substitute <repo-root> with the absolute path of the current repo. Substitute <plugin-source-N> lines with one entry per plugin source directory the daemon will pass via --plugin-dir (Step 13a's dev_mode decision dictates whether these are in-repo <repo-root>/claude/<plugin>/ paths or ~/.claude/plugins/cache/... paths — list what the supervisor unit will actually use).
{
"sandbox": {
"enabled": true,
"filesystem": {
"allowRead": ["<repo-root>", "<plugin-source-1>", "<plugin-source-2>", "..."],
"allowWrite": ["<repo-root>"]
}
},
"additionalDirectories": ["<plugin-source-1>", "<plugin-source-2>", "..."],
"permissions": {
"allow": ["Read", "Write", "Edit", "Glob", "Grep", "Bash", "Skill", "TaskCreate", "TaskUpdate", "TaskList", "TaskGet"],
"deny": ["Bash(find /*)", "Bash(find /Users/*)", "Bash(grep -r /*)", "Bash(grep -R /*)", "Bash(rg /*)", "Bash(rg --files /*)", "Bash(ls /Users/*)"]
}
}
Tilde-form (~/...) is acceptable for paths the operator wants portable across machines — Claude Code expands ~ at load time. Absolute paths are equally valid.
Read <repo-root>/.claude/settings.local.json. Three cases:
Write the recommended block verbatim as a new file. State created.sandbox / permissions / additionalDirectories keys → Edit the file to add all three blocks (preserve every existing top-level key). State appended.AskUserQuestion:
sandbox / additionalDirectories collision → ask once: "Union the recommended paths into the existing list, or keep the existing list verbatim?" Options: "Union" / "Keep local". On Union, Edit to add only the paths that aren't already present. On Keep local, leave that key alone.permissions.allow collision → same wizard: "Union the recommended tool names with the existing allow list?" Options: "Union" / "Keep local".permissions.deny collision → same wizard.
State merged-N where N is the number of keys that got unions; kept-local-N for those left untouched.Never replace an entire key with the recommended value. The operator's existing settings.local.json is authoritative for shape; this skill only adds missing scope.
One line per write action: created / appended / merged-N keep-local-K / skipped-per-user-choice / skipped-not-daemon-repo.
Report to the user:
<version> / <gitCommitSha> (from installed_plugins.json)<path><path> (Step 4)agent_models seed outcome from Step 6.logs/ directory + .lazyignore seed bootstrap outcome (Step 7)migrated or no-stale-entries).memory/ directory bootstrap outcome (Step 10.5)Log to ./.logs/claude/lazy-core.install/YYYY-MM-DD_HH-MM-SS.md per the logging rule (include git_sha frontmatter).
Use two separate steps: Bash(mkdir -p ...) then the Write tool. Never chain with && or use cat > file <<'EOF'.
/lazy-core.install aborts: "plugin isn't actually installed — enable it first" — lazycortex-core@lazycortex is missing from enabledPlugins in ~/.claude/settings.json, or the marketplace entry for lazycortex is absent from extraKnownMarketplaces → add both blocks to ~/.claude/settings.json, restart Claude Code, then re-run./lazy-core.install aborts: "plugin cache is empty — run /plugin update first" — the rule glob under the plugin's installPath returned zero files → run /plugin update lazycortex-core@lazycortex to refresh the cache, then re-run.templates/core/ directory inside the plugin cache is missing or empty → run /plugin update lazycortex-core@lazycortex, then re-run.lazy-core.agent-models/default-tiers.json cannot be read or parsed → reinstall lazycortex-core to restore the file, then re-run..logs/ or .runtime/ not a directory — a file by either of those names already exists at the repo root → remove or rename it, then re-run..gitignore unwritable — bootstrap_logs_dir raised a permission or I/O error → check permissions on the repo root, then re-run.lazy_settings.save_section raises a permission or I/O error when writing the flat daemon / routines sections into .claude/lazy.settings.json → check file permissions on .claude/lazy.settings.json and the .claude/ directory, then re-run.expert_protocol: frontmatter were found under any of the three discovery scopes → no experts are available to register; the wizard skips automatically.parse-error; fix the frontmatter manually and re-run /lazy-core.install to pick it up.reference_resolver.resolve_reference returns None or raises for a candidate's expert_protocol: value → the candidate is skipped and flagged as protocol-unresolvable; verify the protocol file exists at the referenced path or reinstall the owning plugin.${CLAUDE_PLUGIN_ROOT}/templates/runtime/com.lazycortex.runtime.plist or lazy-core-runtime.service is missing from the plugin cache → run /plugin update lazycortex-core@lazycortex to restore templates, then re-run.launchctl load error — the plist was written but launchctl load returned a non-zero exit code → inspect the plist at ~/Library/LaunchAgents/ for substitution errors, then run launchctl load <path> manually.systemctl --user enable --now error — the service unit was written but systemctl returned a non-zero exit code → run systemctl --user status lazy-core-runtime-<REPO_NAME>.service to inspect the error, then correct and re-enable manually.<dev_mode> was persisted as True but <repo-root>/claude/ contains no */.claude-plugin/plugin.json files. The shim silently no-ops the scan and the daemon falls back to the cache — no error, but dev-mode is doing nothing. Either disable dev-mode by re-running /lazy-core.install and answering "No", or populate <repo-root>/claude/<plugin>/.claude-plugin/plugin.json if this is meant to be a plugin-authoring vault./plugin update: /plugin update refreshes the plugin cache but does not re-sync rule files into .claude/rules/. Re-run this skill after every plugin update to pick up rule changes — otherwise projects keep running the old rule content.user writes to ~/.claude/, project writes to <repo-root>/.claude/). Steps 9–13 always target the current working repo (cwd's git toplevel) regardless of install scope, because runtime artifacts (.experts/, daemon supervisor units) are inherently per-repo. Run /lazy-core.install from inside each repo where you want runtime to be set up.git clone: rules/templates/lazy.settings.json/lazy.runtime.sh are committed into the repo, but the daemon supervisor units (launchd plist / systemd service) are per-user and not in the repo. Re-run this skill after cloning to install the supervisor for the current machine and to pick up any newer plugin shipped versions. Pick Skip — no runtime setup in 9a or Skip — this repo doesn't need runtime/experts in 9b if you don't want runtime in this repo at all.npx claudepluginhub mebius-san/lazy-cortex --plugin lazycortex-coreCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.