* fix: promote post-execution hook dispatch to H2 with directive language
Restructure the after_* hook dispatch in all five core command templates
(specify, clarify, implement, plan, tasks) to make hook execution
structurally unmissable by LLMs during interactive slash-command flows.
Changes applied consistently across all five templates:
1. Promote hook block from a final numbered step to a top-level
'## Mandatory Post-Execution Hooks' H2, placed before the completion
report. An H2 boundary is salient in a way that a numbered sub-step
buried after 'Report completion' is not.
2. Use directive language for mandatory hooks: 'You MUST emit
EXECUTE_COMMAND: for each mandatory hook' and 'You MUST complete this
section before reporting completion to the user.' The previous
conditional framing ('check if', 'based on its optional flag') buried
the mandatory cases.
3. List mandatory hooks before optional hooks in the dispatch block, so
the required action appears first.
4. Add a terminal '## Done When' verification checklist at the end of
each template, including 'Extension hooks dispatched' as an explicit
completion criterion. This gives the model a structured opportunity to
verify completion before exiting.
5. Extract the completion report into its own '## Completion Report' H2
section, clearly separated from the hook dispatch.
These changes preserve the interactive-vs-workflow distinction and do not
introduce auto-run. They raise the reliability of the existing
best-effort dispatch mechanism.
Fixes#2688
* fix: address review — narrow Done When wording and move to end of templates
- Narrow the hooks checklist item from a broad condition to 'dispatched
or skipped according to the rules in Mandatory Post-Execution Hooks
above' so it does not contradict the filtering rules for disabled
hooks or hooks with non-empty conditions.
- Move the Done When section to the actual end of specify.md, plan.md,
and tasks.md so it does not signal premature completion before the
agent reads required guidance sections (Quick Guidelines, Phases/Key
rules, Task Generation Rules).
* fix: update stale step 8 reference in specify.md validation flow
The old 'proceed to step 8' pointed to the completion report step that
was renumbered when hook dispatch was promoted to its own H2 section.
Update the reference to point to 'Mandatory Post-Execution Hooks'.
* fix: create skills directory on demand during extension/preset install
_get_skills_dir() in both extensions.py and presets.py returned None
when the skills directory did not yet exist on disk, even though skills
were enabled in init-options. This caused extension skill registration
to silently produce an empty registered_skills list and skip writing
SKILL.md files.
Replace the is_dir() bail-out with mkdir(parents=True, exist_ok=True)
so the directory is created on demand when ai_skills is enabled.
Update the existing test expectation and add a parametrized regression
test (claude + codex) that installs an extension before the skills
directory exists and asserts SKILL.md files and registry entries are
created.
Fixes#2682
* test: assert skills dir is NOT created when skills are disabled
Strengthen negative tests to verify _get_skills_dir does not create the
directory on disk when ai_skills is false or init-options.json is absent.
* fix: add symlink/containment check and preserve Kimi existence gate
Address PR review feedback:
- Use _ensure_safe_shared_directory() instead of raw mkdir() to prevent
symlink-following writes outside the project root.
- Restore the is_dir() existence gate for the Kimi native-skills
fallback (ai_skills=false): only create the directory on demand when
ai_skills is explicitly enabled.
- Update docstrings to reflect the on-demand vs existence-gate behavior.
- Reuse resolve_skills_dir helper in tests instead of manually
reconstructing paths from AGENT_CONFIG.
* refactor: extract resolve_active_skills_dir shared helper
Deduplicate the _get_skills_dir logic that was nearly identical in
ExtensionManager and PresetManager into a single module-level
resolve_active_skills_dir() function in __init__.py.
The shared helper wraps _ensure_safe_shared_directory errors with
skills-specific messages so users see 'agent skills directory' instead
of 'shared infrastructure directory' in error output.
Both class methods now delegate to the shared helper.
* fix: preserve original error reason in skills dir safety check
Include the original exception message from _ensure_safe_shared_directory
in the re-raised ValueError so the user sees the specific reason (symlink,
not-a-directory, path escape, etc.) instead of a generic message.
* fix: handle skills dir safety errors gracefully during install
Catch ValueError/OSError from _get_skills_dir() inside
_register_extension_skills() so a symlink or permission error logs a
warning and returns [] instead of aborting mid-install and leaving a
partially-installed extension without a registry entry.
Also document OSError in resolve_active_skills_dir() docstring.
* fix: catch errors in _get_skills_dir and use _print_cli_warning
Move the ValueError/OSError catch from _register_extension_skills into
_get_skills_dir itself so all callers (install, uninstall, reconcile)
are protected from unsafe-path exceptions.
Replace logging.getLogger().warning with _print_cli_warning for
consistent Rich-formatted user output.
* fix: use context-aware error messages for skills directory safety
Add a 'context' parameter to _ensure_safe_shared_directory (defaults to
'shared infrastructure directory' for backward compat). The skills dir
caller passes context='agent skills directory' so error messages say
e.g. 'Refusing to use symlinked agent skills directory' instead of
'Refusing to use symlinked shared infrastructure directory'.
Simplify resolve_active_skills_dir by removing the now-unnecessary
try/except wrapper.
* fix: validate Kimi native-skills directory for symlink/containment
The Kimi fallback path (ai_skills=false) used is_dir() which follows
symlinks, so a symlinked .kimi/skills could cause writes outside the
project root. Now validates with _ensure_safe_shared_directory(create=
False) before returning the directory.
* chore: bump version to 0.8.14
* chore: begin 0.8.15.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* refactor: create commands/ package and move init handler (PR-4/8)
- Extract agent configuration constants (AGENT_CONFIG, AI_ASSISTANT_HELP,
SCRIPT_TYPE_CHOICES, etc.) to _agent_config.py to avoid circular imports
- Create commands/ package skeleton with stub modules for each command group
- Move init command handler (~670 lines) from __init__.py to commands/init.py
using the register(app) pattern; lazy imports inside the handler body
prevent circular dependencies with __init__.py
- Re-export AGENT_CONFIG, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES from
__init__.py for backward compatibility
- Add tests/test_commands_package.py to verify package structure
* fix(tests): update patch targets after moving init handler to commands/init.py
_stdin_is_interactive and select_with_arrows are now bound in
specify_cli.commands.init, not specify_cli directly.
* fix(lint): remove unused imports and mark re-exports in __init__.py
- Remove shutil, shlex top-level imports (used lazily inside functions)
- Remove rich.live.Live import (moved to commands/init.py)
- Mark select_with_arrows and _locate_bundled_workflow as explicit
re-exports to satisfy ruff F401
* chore: add from __future__ import annotations to new modules
Aligns with the project convention established in _console.py, _assets.py,
_utils.py, and other modules.
* docs(cli): align init help with bundled scaffolding
Potential fix for pull request finding
Update command package documentation and init help text to reflect the current implementation: init uses bundled assets and integration setup, while placeholder command modules are import anchors until extracted. Remove the unused tracker-active flag assignment that had no reader in the codebase.
Constraint: --offline is hidden/no-op and init no longer downloads templates from GitHub releases
Rejected: Add no-op register functions to placeholder modules | would imply extracted command groups are implemented there
Confidence: high
Scope-risk: narrow
Directive: Keep CLI help text aligned with the actual init scaffolding path
Tested: uv run specify init --help; uv run pytest tests/test_commands_package.py tests/test_agent_config_consistency.py -q; uv run pytest tests/test_commands_package.py tests/test_console_imports.py tests/integrations/test_cli.py -q
Not-tested: full test suite
* fix(init): align preset failure reporting with _print_cli_warning helper
Use the _print_cli_warning helper (introduced in main) for preset install
failures so that output matches the expected format:
"Failed to install preset '<name>': ..."
"Continuing without the optional preset."
* fix(init): remove unused lazy imports
The init command imported CLI error formatting helpers through its circular-dependency-safe lazy import block, but the module does not use them. Remove those imports so ruff does not report F401.
Constraint: uvx ruff check src/ must pass.
Rejected: Wire the helpers into init error handling | Existing preset warnings already use _print_cli_warning, and changing behavior is unnecessary for this lint fix.
Confidence: high
Scope-risk: narrow
Directive: Keep lazy import blocks limited to names consumed in the importing module.
Tested: uvx ruff check src/
Not-tested: Full pytest suite
Co-authored-by: OmX <omx@oh-my-codex.dev>
---------
Co-authored-by: OmX <omx@oh-my-codex.dev>
- AGENTS.md: branch naming as a requirement for AI coding agents
- CONTRIBUTING.md: branch naming as a recommendation for human contributors
- Convention: <type>/<number>-<short-slug> where number is issue or PR number
Closes#2677
* chore: bump version to 0.8.13
* chore: begin 0.8.14.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: while/do-while loop condition reads stale iteration-0 step output
After executing namespaced loop body steps, copy each iteration's
results back to the original unprefixed step key so that
evaluate_condition() sees the latest values instead of stale
iteration-0 data.
Fixes#2592
* address review: cross-platform tests, preserve iteration-0 history
- Rewrite shell scripts in tests to use Python via script files
instead of POSIX syntax, so they pass on Windows CI.
- Snapshot iteration-0 nested-step results under a namespaced key
(parent:child:0) before the first copy-back overwrite, preserving
complete per-iteration history for debugging.
* address review: skip copy-back on paused/failed iterations
Move the status check before the copy-back so that partial results
from paused or failed nested steps (e.g., a gate awaiting input)
do not overwrite the unprefixed key. This preserves correct resume
behavior.
* address review: quote paths in test shell commands
Quote both the Python executable and script file paths in the
run: commands to handle spaces in paths on Windows.
* address review: execute loop body with original IDs
Instead of namespacing step IDs for execution and copying results
back, execute the loop body with original (unprefixed) step IDs so
results naturally land at the right keys. Snapshot previous
iteration results to namespaced keys (parent:child:N) for history
only.
This fixes multi-step loop bodies where step B references step A's
output within the same iteration — previously step B would see
stale data until the copy-back ran after the entire iteration.
* address review: namespaced execution with per-step copy-back
Revert to namespaced step IDs for execution (preserving unique
log entries and state keys per iteration) but copy each step's
result back to the unprefixed key immediately after it completes.
This preserves backward compatibility (same namespaced key format,
same log IDs) while fixing both the condition evaluation bug and
inter-step references within multi-step loop bodies.
* address review: alias after status check, add multi-step body test
- Move per-step aliasing below the PAUSED/FAILED/ABORTED status
check so partial results from incomplete steps are not aliased
back to the unprefixed key.
- Add test_while_loop_multi_step_body_inter_step_refs to exercise
a multi-step loop body where step B reads step A's output within
the same iteration, verifying per-step aliasing works correctly.
Addresses feedback from @doquanghuy (items 2 & 4) and Copilot
review on commit 9d0a222.
* address review: stable fallback IDs, expression-based inter-step test
- Use enumerate() for stable fallback IDs when loop body steps lack
an explicit id (step-0, step-1, etc. instead of always step-0).
- Rewrite multi-step body test so step B uses expression
substitution ({{ steps.step-a.output.stdout }}) instead of
reading the counter file directly, making it a true regression
test for per-step aliasing.
`bool` is a subclass of `int` in Python, so `int(True)` silently returns
`1`. The extension- and preset-catalog config readers coerced priority
with a bare `int(item.get("priority", idx + 1))`, which meant a YAML
config like:
catalogs:
- name: mine
url: https://example.com/catalog.json
priority: yes # parses to True
was silently accepted as a valid priority of 1, quietly reordering the
catalog stack instead of raising the same `Invalid priority` error a
typo of `priority: not-a-number` already raises.
The sibling integration-catalog reader in `src/specify_cli/catalogs.py`
already guards this case (see `catalogs.py:137`). This change mirrors
that pattern in `extensions.py` and `presets.py` so the three catalog
validators stay consistent, and adds regression tests for both readers
matching the existing `test_load_catalog_config_rejects_boolean_priority`
template in `tests/integrations/test_integration_catalog.py`.
* Add agentic workflows for community catalog submissions
Add GitHub Agentic Workflows that automatically process community
extension and preset submission issues:
- add-community-extension.md: triggered by extension-submission issues,
validates the submission, updates extensions/catalog.community.json
and docs/community/extensions.md, then opens a draft PR
- add-community-preset.md: parallel workflow for preset-submission
issues, updates presets/catalog.community.json and
docs/community/presets.md
Both workflows:
- Trigger on opened, edited, or labeled events (maintainers can
retroactively label pre-existing issues)
- Validate ID format, semver, repo existence, required files, release,
and submission checklists
- Label issues with validation-passed or validation-failed
- Create draft PRs with Closes #N for maintainer review
Also includes gh-aw scaffolding (.github/aw/, .gitattributes lock file
rule, dependabot ignore for gh-aw-actions).
* Suppress whitespace checks on generated .lock.yml files
These files are auto-generated by gh aw compile and contain trailing
whitespace in the ASCII art header and indented YAML blocks that we
cannot control. Add -whitespace attribute to skip git whitespace
checks on them.
* feat: add self-check tip to check output
* style: drop trailing period from self-check tip
Aligns the new tip with the other `Tip:` lines in `specify check`,
which don't end in a period. Per Copilot review feedback on #2574.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Consolidate the CLI diagnostic plan, implementation, and test hardening into one reviewable change. The CLI now reports phase and target context for broad failure paths while preserving existing fail-fast behavior for real setup failures and warning-only behavior for optional best-effort work.
The workflow unit tests also avoid discovering real local agent CLIs, so developer machines with tools such as gemini installed do not hang pytest during metadata-only assertions.
Constraint: CLI setup failures must remain fail-fast, while optional preset and cleanup paths should continue with clear warnings.
Rejected: Replace broad handlers across the whole codebase in one pass | too broad for a targeted CLI diagnostic fix
Rejected: Add runtime timeouts to workflow agent dispatch | dispatch may legitimately be long-running and the observed hang was test isolation
Confidence: high
Scope-risk: moderate
Directive: Keep future best-effort CLI warnings tied to the failed phase and target so users can diagnose setup state.
Tested: uvx ruff check src/; uv run pytest tests/integrations/test_cli.py -v; uv run pytest tests/test_workflows.py::TestCommandStep::test_step_override_integration tests/test_workflows.py::TestPromptStep::test_execute_with_step_integration tests/test_workflows.py::TestPromptStep::test_execute_with_model -vv; uv run pytest
Not-tested: Real Nacos/PG/Redis-style external service failure injection; real interactive workflow dispatch against installed gemini CLI
* chore: bump version to 0.8.12
* chore: begin 0.8.13.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(codex): inject dot-to-hyphen hook command note in Codex skills
Hook commands in `.specify/extensions.yml` use dotted ids like
`speckit.git.commit`, but Codex skills are named with hyphens
(`speckit-git-commit`). The Claude integration handles this via an
explicit instruction injected into each generated SKILL.md by
`ClaudeIntegration.post_process_skill_content`, but the Codex
integration had no such override, so Codex would emit
`/speckit.git.commit` (which does not resolve) instead of
`/speckit-git-commit`.
This adds the same `_inject_hook_command_note` helper and a
`post_process_skill_content` override to `CodexIntegration`, plus a
small `setup()` override that applies the post-process to each
generated SKILL.md (mirroring the pattern in `ClaudeIntegration`).
Also widens the existing
`test_non_claude_post_process_is_identity` test to use `agy`
(another `SkillsIntegration` with no override), since asserting
identity behavior on Codex would now incorrectly fail.
Tests:
- New `TestCodexHookCommandNote` class mirrors
`TestClaudeHookCommandNote`: setup-level injection, no-op when
no hook block is present, idempotency, and indentation
preservation.
- `pytest tests/` → 2866 passed, 34 skipped.
Signed-off-by: Chao Zhang <1175468+picklebento@users.noreply.github.com>
* fix(codex): handle empty eol when instruction is final line without newline
The hook-note injection regex allowed end-of-string matches via ``$``,
which left the captured ``eol`` empty. When the matched indent was also
empty, the substitution concatenated the note onto the same line as the
instruction. Default ``eol`` to ``\n`` when the capture is empty.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Signed-off-by: Chao Zhang <1175468+picklebento@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(workflow): support integration: auto to follow project's initialized AI
Closes#2406
(squashed)
* fix(workflow): combine JSONDecodeError and UnicodeDecodeError handling
Address Copilot feedback: UnicodeDecodeError can be raised by both
read_text() and json.loads(), so combining the handlers ensures both
cases produce a consistent, clear error message.
* fix(workflows): honor integration_state schema guard and modern state in 'integration: auto'
Three Copilot follow-ups on PR #2421:
1. engine.py:799 — `_load_project_integration` was bypassing the same
schema guard `_read_integration_json` enforces. It now reads the
schema field directly, returns None on a future schema (so the
workflow falls back to the literal 'auto' default rather than
guessing), and routes through `normalize_integration_state` /
`default_integration_key` so modern installs that record
`default_integration` / `installed_integrations` (without the
legacy top-level `integration` field) resolve correctly.
2. test_workflows.py — added two regression cases:
- `integration: auto` resolves a modern normalized state file
- `integration: auto` falls back when the state file declares a
newer `integration_state_schema` than this CLI supports
3. test_cli.py — added a CLI-level regression for the `UnicodeDecodeError`
branch in `_read_integration_json` to match the existing
malformed-JSON coverage.
* refactor(integration): extract shared try_read_integration_json helper
Address Copilot review on PR #2421:
Both `_read_integration_json` (CLI) and `_load_project_integration`
(workflow engine) were parsing `.specify/integration.json` independently,
duplicating the schema guard and risking drift between the two readers.
Extract the parse + schema validation into a single low-level helper
`try_read_integration_json` in `integration_state.py` that returns either
the normalized state or a structured `IntegrationReadError`. Both callers
now delegate to this helper:
- CLI keeps its loud-fail UX: each error kind ("decode", "os",
"not_object", "schema_too_new") is translated into the existing console
message + typer.Exit(1).
- Engine keeps its silent fallback: any error simply returns None so
`integration: auto` falls back to the workflow's literal default.
This eliminates the divergence Copilot flagged without changing observable
behavior for either caller.
* fix(integration): distinguish missing file from non-regular path
Address Copilot review on PR #2421:
`try_read_integration_json` was collapsing two distinct cases into a
single `(None, None)` return:
1. `.specify/integration.json` truly missing — silent fallback is correct.
2. Path exists but is a directory, socket, or other non-regular file —
this is a misconfiguration the CLI should surface loudly.
Split the check: `exists()` falsey returns `(None, None)`; existing-but-
not-a-regular-file returns `(None, IntegrationReadError(kind="os", ...))`
so the CLI's loud-fail path produces an actionable error while the
engine still treats it as a fallback to the workflow's literal default.
* docs(workflow): clarify version pin, advisory integrations list, enum exemption
- workflow.yml: fix comment that said 0.8.3 was first release with auto
resolution; the pin is >=0.8.5 so the comment now matches the pin.
- workflow.yml: clarify that requires.integrations.any is an advisory,
non-exhaustive compatibility hint, not a closed set.
- engine.py: clarify that the auto-sentinel exemption only skips enum
membership; declared type is still enforced through _coerce_input.
* fix(workflow): resolve auto sentinel for provided values; report stat errors
Two Copilot findings fixed:
1. _resolve_inputs only resolved the ``integration: auto`` sentinel when it
came from the input default. A caller explicitly providing
``{"integration": "auto"}`` (which the workflow prompt advertises as a
valid value) bypassed _resolve_default and the literal "auto" leaked
to dispatch. Provided values now go through the same resolution path
as defaults, and the enum-membership exemption applies in both cases.
Regression test added.
2. try_read_integration_json used Path.exists() / Path.is_file() as a
pre-check. Both return False on some OSErrors (e.g. permission errors
during stat), which silently treated an unreadable-but-present file
as missing — the engine fell back without warning and the CLI failed
to surface the loud error. The pre-check is gone: read_text() is
attempted directly, FileNotFoundError means missing (silent fallback),
IsADirectoryError and other OSErrors become loud IntegrationReadError.
* fix(workflow): enforce declared type for string inputs, reject bool-as-number
Two Copilot findings fixed:
1. _coerce_input previously coerced/validated only ``number`` and
``boolean`` types, so ``type: string`` silently accepted any Python
value (numbers, lists, dicts). A YAML authoring mistake like
``type: string`` + ``default: 5`` slipped through. Strings are now
required to actually be strings; non-strings raise ValueError, which
surfaces as an ``invalid default`` error from validate_workflow.
2. ``type: number`` accepted ``default: true`` because ``bool`` is a
subclass of ``int`` (``float(True) == 1.0``). Bools are now rejected
explicitly in the number path so the YAML mistake fails fast. The
boolean path is also tightened to reject non-bool / non-string
values for symmetry.
Comment on the auto-sentinel enum exemption updated to reflect the
stronger guarantee. Regression tests added for both rejections.
* fix(cli): drop unused normalize_integration_state import to satisfy ruff
CI's `uvx ruff check src/` flagged this as F401: the symbol was imported
under a private alias but never referenced. Tests stay green after
removal.
* chore: bump version to 0.8.11
* chore: begin 0.8.12.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* refactor: extract _version.py from __init__.py (PR-3/8)
Move version-checking helpers and `specify self` sub-commands into a
focused `_version.py` module.
Moved symbols:
- GITHUB_API_LATEST — GitHub releases API endpoint constant
- _get_installed_version — importlib.metadata-based version lookup
- _normalize_tag — strip leading 'v' from release tag strings
- _is_newer — PEP 440 version comparison
- _fetch_latest_release_tag — single outbound call to GitHub API
- self_app — Typer sub-app for `specify self`
- self_check, self_upgrade — `specify self check/upgrade` commands
Dependency rule: _version.py imports only stdlib + packaging + ._console.
Backward compatibility: GITHUB_API_LATEST, self_check, self_upgrade
remain importable from specify_cli via re-exports in __init__.py.
Update test_upgrade.py to import helpers from specify_cli._version and
patch at the correct module path (specify_cli._version.*).
Add test_version_imports.py as regression guard.
* fix(tests): update _fetch_latest_release_tag import path in test_authentication.py
PR-3 moved _fetch_latest_release_tag from specify_cli into
specify_cli._version. test_upgrade.py was updated at the time, but
test_authentication.py::TestFetchLatestReleaseTagDelegation still
imported from the old location, causing ImportError on all three
delegation tests. Update all three inline imports to the correct
module path.
* Add Time Machine extension to community catalog
Add time-machine extension submitted by @teeyo to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#2568
* Fix alphabetical ordering of time-machine entry
Move time-machine before tinyspec in both catalog JSON (by ID)
and docs table (by name), since time < tiny alphabetically.
* docs: fix script name in directory tree examples
Replace update-claude-md.sh with the actual filename setup-tasks.sh
in two directory tree examples (Steps 2 and 4 of the detailed walkthrough).
* docs: fix .specify/scripts layout to show bash/ and powershell/ subdirs
install_shared_infra() installs scripts under .specify/scripts/bash/
(script_type="sh") or .specify/scripts/powershell/ (script_type="ps"),
not directly under .specify/scripts/. Update docs/installation.md and
the _install_shared_infra docstring to reflect the actual on-disk layout.
* docs: update README tree examples to show scripts/bash/ subdirectory
Scripts are installed under .specify/scripts/bash/ (or powershell/)
not directly under .specify/scripts/. Fix both tree diagrams in the
Detailed Process walkthrough to match the actual on-disk layout.
* chore: bump version to 0.8.10
* chore: begin 0.8.11.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
- Shorten README.md install section to single uv command + link to
installation guide for alternatives and troubleshooting
- Add explicit 'Initialize a project' step to README Get Started
- Remove duplicate Troubleshooting section from README
- Reorder 'Make it your own' card on docs landing page so extensions
and presets are explained before the stats
- Update Community nav-card to link to new community overview
- Create docs/community/overview.md landing page (aligned with
reference/overview.md)
- Create dedicated install sub-pages: pipx, one-time (uvx), air-gapped
- Update docs/installation.md to lead with persistent uv install and
link to sub-pages instead of duplicating content
- Update docs/toc.yml with new pages
- Remove stale EOF file