diff --git a/.agents/skills/oat-agent-instructions-analyze/SKILL.md b/.agents/skills/oat-agent-instructions-analyze/SKILL.md index 32e9f3984..304d2839b 100644 --- a/.agents/skills/oat-agent-instructions-analyze/SKILL.md +++ b/.agents/skills/oat-agent-instructions-analyze/SKILL.md @@ -77,6 +77,7 @@ or fill in missing evidence gaps on its own. ```bash SCRIPT_DIR=".agents/skills/oat-agent-instructions-analyze/scripts" +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" PROVIDERS=$(bash "$SCRIPT_DIR/resolve-providers.sh" --non-interactive) # Or with explicit override: # PROVIDERS=$(bash "$SCRIPT_DIR/resolve-providers.sh" --providers claude,cursor) @@ -87,7 +88,7 @@ If running interactively (user invoked the skill directly), omit `--non-interact **Resolve analysis mode (delta vs full):** ```bash -TRACKING=$(bash "$SCRIPT_DIR/resolve-tracking.sh" read agentInstructions) +TRACKING=$(bash "$TRACKING_SCRIPT" read agentInstructions) ``` - If `TRACKING` is non-empty, extract `commitHash` from the JSON. @@ -439,11 +440,12 @@ The markdown artifact and companion bundle together are the contract for apply. **Update tracking:** ```bash -ROOT_TARGET=$(bash "$SCRIPT_DIR/resolve-tracking.sh" root) +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" +ROOT_TARGET=$(bash "$TRACKING_SCRIPT" root) ROOT_HASH=$(echo "$ROOT_TARGET" | jq -r '.commitHash') ROOT_BRANCH=$(echo "$ROOT_TARGET" | jq -r '.baseBranch') -bash "$SCRIPT_DIR/resolve-tracking.sh" write \ +bash "$TRACKING_SCRIPT" write \ agentInstructions \ "$ROOT_HASH" \ "$ROOT_BRANCH" \ @@ -493,6 +495,6 @@ Next step: Run oat-agent-instructions-apply to act on these findings. - Bundle summary template: `references/bundle-summary-template.md` - Bundle manifest template: `references/recommendations-manifest-template.yaml` - Recommendation pack template: `references/recommendation-pack-template.md` -- Tracking script: `scripts/resolve-tracking.sh` +- Tracking script: `.oat/scripts/resolve-tracking.sh` - Provider resolution: `scripts/resolve-providers.sh` - File discovery: `scripts/resolve-instruction-files.sh` diff --git a/.agents/skills/oat-agent-instructions-apply/SKILL.md b/.agents/skills/oat-agent-instructions-apply/SKILL.md index 98fcc0418..397861d01 100644 --- a/.agents/skills/oat-agent-instructions-apply/SKILL.md +++ b/.agents/skills/oat-agent-instructions-apply/SKILL.md @@ -156,6 +156,7 @@ Then stop. ```bash SCRIPT_DIR=".agents/skills/oat-agent-instructions-analyze/scripts" +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" PROVIDERS=$(bash "$SCRIPT_DIR/resolve-providers.sh" --non-interactive) ``` @@ -460,12 +461,12 @@ PR creation failed. To create manually: **Update tracking:** ```bash -SCRIPT_DIR=".agents/skills/oat-agent-instructions-analyze/scripts" -ROOT_TARGET=$(bash "$SCRIPT_DIR/resolve-tracking.sh" root) +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" +ROOT_TARGET=$(bash "$TRACKING_SCRIPT" root) ROOT_HASH=$(echo "$ROOT_TARGET" | jq -r '.commitHash') ROOT_BRANCH=$(echo "$ROOT_TARGET" | jq -r '.baseBranch') -bash "$SCRIPT_DIR/resolve-tracking.sh" write \ +bash "$TRACKING_SCRIPT" write \ agentInstructionsApply \ "$ROOT_HASH" \ "$ROOT_BRANCH" \ @@ -503,4 +504,4 @@ Apply complete. - Analysis artifact: `.oat/repo/analysis/agent-instructions-*.md` - Templates: `references/instruction-file-templates/` - Apply plan template: `references/apply-plan-template.md` -- Tracking script: `scripts/resolve-tracking.sh` (symlink to analyze skill) +- Tracking script: `.oat/scripts/resolve-tracking.sh` diff --git a/.agents/skills/oat-agent-instructions-apply/scripts/resolve-tracking.sh b/.agents/skills/oat-agent-instructions-apply/scripts/resolve-tracking.sh deleted file mode 120000 index 6b2433f4f..000000000 --- a/.agents/skills/oat-agent-instructions-apply/scripts/resolve-tracking.sh +++ /dev/null @@ -1 +0,0 @@ -../../oat-agent-instructions-analyze/scripts/resolve-tracking.sh \ No newline at end of file diff --git a/.agents/skills/oat-docs-analyze/SKILL.md b/.agents/skills/oat-docs-analyze/SKILL.md index 62045d9a5..58b9ab2a5 100644 --- a/.agents/skills/oat-docs-analyze/SKILL.md +++ b/.agents/skills/oat-docs-analyze/SKILL.md @@ -92,7 +92,7 @@ Prefer the OAT docs app when multiple MkDocs apps exist and one is clearly the a Resolve tracking and analysis mode using the shared helper: ```bash -TRACKING_SCRIPT=".agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh" +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" TRACKING=$(bash "$TRACKING_SCRIPT" read docs 2>/dev/null || true) ``` @@ -307,7 +307,7 @@ Populate the artifact with: Update docs tracking using the shared helper: ```bash -TRACKING_SCRIPT=".agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh" +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" ROOT_TARGET=$(bash "$TRACKING_SCRIPT" root) ROOT_HASH=$(echo "$ROOT_TARGET" | jq -r '.commitHash') ROOT_BRANCH=$(echo "$ROOT_TARGET" | jq -r '.baseBranch') @@ -352,4 +352,4 @@ Next step: Run oat-docs-apply to act on these findings. - Analysis artifact template: `references/analysis-artifact-template.md` - Quality checklist: `references/quality-checklist.md` - Directory criteria: `references/directory-assessment-criteria.md` -- Shared tracking helper: `.agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh` +- Shared tracking helper: `.oat/scripts/resolve-tracking.sh` diff --git a/.agents/skills/oat-docs-apply/SKILL.md b/.agents/skills/oat-docs-apply/SKILL.md index 043230870..6068325b2 100644 --- a/.agents/skills/oat-docs-apply/SKILL.md +++ b/.agents/skills/oat-docs-apply/SKILL.md @@ -301,7 +301,7 @@ PR creation failed. To create manually: Update shared tracking: ```bash -TRACKING_SCRIPT=".agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh" +TRACKING_SCRIPT=".oat/scripts/resolve-tracking.sh" ROOT_TARGET=$(bash "$TRACKING_SCRIPT" root) ROOT_HASH=$(echo "$ROOT_TARGET" | jq -r '.commitHash') ROOT_BRANCH=$(echo "$ROOT_TARGET" | jq -r '.baseBranch') @@ -336,4 +336,4 @@ Next step: Re-run oat-docs-analyze if you want a post-apply verification artifac ## References - Apply plan template: `references/apply-plan-template.md` -- Shared tracking helper: `.agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh` +- Shared tracking helper: `.oat/scripts/resolve-tracking.sh` diff --git a/.agents/skills/oat-doctor/SKILL.md b/.agents/skills/oat-doctor/SKILL.md index 930202098..8f5fe8dfb 100644 --- a/.agents/skills/oat-doctor/SKILL.md +++ b/.agents/skills/oat-doctor/SKILL.md @@ -86,7 +86,7 @@ Read `$ARGUMENTS`: TOOLS_JSON=$(oat tools list --json --scope all 2>/dev/null || echo '{"tools":[]}') ``` -Parse the JSON output. Each tool has: `name`, `type` (skill/agent), `scope` (project/user), `version`, `bundledVersion`, `pack` (core/ideas/workflows/utility/custom), `status` (current/outdated/newer/not-bundled). +Parse the JSON output. Each tool has: `name`, `type` (skill/agent), `scope` (project/user), `version`, `bundledVersion`, `pack` (core/docs/ideas/workflows/utility/project-management/research/custom), `status` (current/outdated/newer/not-bundled). ### Step 2: Check for Outdated Skills @@ -167,13 +167,27 @@ Ideas pack skills: - oat-idea-new, oat-idea-ideate, oat-idea-summarize, oat-idea-scratchpad -Utility pack skills: +Docs pack skills: -- create-agnostic-skill, oat-agent-instructions-analyze, oat-agent-instructions-apply +- oat-agent-instructions-analyze, oat-agent-instructions-apply - oat-docs-analyze, oat-docs-apply + +Utility pack skills: + +- create-agnostic-skill - oat-repo-maintainability-review, oat-review-provide - oat-review-receive, oat-review-receive-remote +Project management pack skills: + +- oat-pjm-add-backlog-item, oat-pjm-update-repo-reference +- oat-pjm-review-backlog + +Research pack skills: + +- analyze, compare, deep-research +- skeptic, synthesize + For each pack, determine: - **Installed:** all pack skills found in installed tools list @@ -205,9 +219,12 @@ OAT ▸ DOCTOR SUMMARY | Pack | Scope | Skills | Status | | --------- | ------- | ------ | -------- | | core | user | 2/2 | current | +| docs | project | 4/4 | current | | workflows | project | 26/26 | current | | ideas | user | 4/4 | current | -| utility | project | 9/9 | outdated | +| project-management | project | 3/3 | current | +| research | project | 5/5 | current | +| utility | project | 5/5 | outdated | ## Outdated Skills @@ -217,8 +234,12 @@ OAT ▸ DOCTOR SUMMARY ## Available But Not Installed -- **utility** pack: oat-docs-analyze, oat-docs-apply (2 skills available) - → Run: oat tools update --scope {scope} +- **docs** pack: oat-docs-analyze, oat-docs-apply, oat-agent-instructions-analyze, oat-agent-instructions-apply (4 skills available) + → Run: oat tools install docs --scope {scope} +- **project-management** pack: oat-pjm-add-backlog-item, oat-pjm-update-repo-reference, oat-pjm-review-backlog (3 skills available) + → Run: oat tools install project-management --scope {scope} +- **research** pack: analyze, compare, deep-research, skeptic, synthesize (5 skills available) + → Run: oat tools install research --scope {scope} ## Configuration diff --git a/.agents/skills/oat-repo-knowledge-index/SKILL.md b/.agents/skills/oat-repo-knowledge-index/SKILL.md index 8e53a0cf2..dbba77dfd 100644 --- a/.agents/skills/oat-repo-knowledge-index/SKILL.md +++ b/.agents/skills/oat-repo-knowledge-index/SKILL.md @@ -675,11 +675,11 @@ Generated from commit: {MERGE_BASE_SHA}" Record the knowledge index run in the shared tracking manifest: ```bash -ROOT_TARGET=$(bash .agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh root) +ROOT_TARGET=$(bash .oat/scripts/resolve-tracking.sh root) ROOT_HASH=$(echo "$ROOT_TARGET" | jq -r '.commitHash') ROOT_BRANCH=$(echo "$ROOT_TARGET" | jq -r '.baseBranch') -bash .agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh \ +bash .oat/scripts/resolve-tracking.sh \ write knowledgeIndex "$ROOT_HASH" "$ROOT_BRANCH" full \ --artifact-path ".oat/repo/knowledge/" ``` diff --git a/.agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh b/.oat/scripts/resolve-tracking.sh old mode 100755 new mode 100644 similarity index 88% rename from .agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh rename to .oat/scripts/resolve-tracking.sh index 97834dde5..e60bcf27f --- a/.agents/skills/oat-agent-instructions-analyze/scripts/resolve-tracking.sh +++ b/.oat/scripts/resolve-tracking.sh @@ -29,11 +29,9 @@ set -euo pipefail -# Resolve repo root and tracking file path REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" TRACKING_FILE="${REPO_ROOT}/.oat/tracking.json" -# Ensure jq is available if ! command -v jq &>/dev/null; then echo "Error: jq is required but not found in PATH" >&2 exit 1 @@ -139,7 +137,6 @@ cmd_write() { local mode="${4:?Missing mode}" shift 4 - # Parse optional --artifact-path flag before variadic formats local artifact_path="" if [[ "${1:-}" == "--artifact-path" ]]; then artifact_path="${2:?Missing artifact path value after --artifact-path}" @@ -148,7 +145,6 @@ cmd_write() { local formats=("$@") - # Normalize tracking target to root branch tip to keep commitHash resolvable. local normalized_branch normalized_hash normalized_branch="$(detect_root_branch)" normalized_hash="$(resolve_root_commit_hash "$normalized_branch")" @@ -160,7 +156,6 @@ cmd_write() { base_branch="$normalized_branch" commit_hash="$normalized_hash" - # Build formats JSON array local formats_json="[]" if [[ ${#formats[@]} -gt 0 ]]; then formats_json=$(printf '%s\n' "${formats[@]}" | jq -R . | jq -s .) @@ -169,7 +164,6 @@ cmd_write() { local timestamp timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - # Read existing or initialize local existing if [[ -f "$TRACKING_FILE" ]] && jq empty "$TRACKING_FILE" 2>/dev/null; then existing="$(cat "$TRACKING_FILE")" @@ -178,7 +172,6 @@ cmd_write() { existing='{"version":1}' fi - # Merge operation entry (include artifactPath only if provided) if [[ -n "$artifact_path" ]]; then echo "$existing" | jq \ --arg op "$operation" \ @@ -216,17 +209,18 @@ cmd_write() { echo "Updated $TRACKING_FILE [$operation]" } -# Dispatch subcommand case "${1:-}" in init) - cmd_init + shift + cmd_init "$@" ;; read) shift cmd_read "$@" ;; root) - cmd_root + shift + cmd_root "$@" ;; write) shift @@ -234,12 +228,6 @@ case "${1:-}" in ;; *) echo "Usage: resolve-tracking.sh {init|read|root|write} [args...]" >&2 - echo "" >&2 - echo "Commands:" >&2 - echo " init Create tracking.json if missing" >&2 - echo " read Read operation entry" >&2 - echo " root Print root branch + commit as JSON" >&2 - echo " write [--artifact-path

] [fmts]" >&2 - exit 1 + exit 2 ;; esac diff --git a/AGENTS.md b/AGENTS.md index 7209d9449..b60bb69ef 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -138,7 +138,7 @@ Projects live in `.oat/projects///` with: - Core files: `state.md`, `discovery.md`, `spec.md`, `design.md`, `plan.md`, `implementation.md` - Optional: `reviews/`, `pr/` -### Utility Skill Additions +### Docs Pack Workflows - `oat-docs-analyze` - Analyze a docs surface for `index.md` contract coverage, nav drift, and docs app readiness. - `oat-docs-apply` - Apply approved docs-analysis recommendations with branch, nav-sync, and PR workflow support. diff --git a/README.md b/README.md index e99ea99af..9f3c81fd3 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ pnpm run cli -- tools update --all Notes: -- `tools install` installs OAT skills/agents/templates/scripts by pack (`core`, `ideas`, `workflows`, `utility`, `research`) and auto-syncs provider views. The core pack (diagnostics, docs) always installs at user scope. +- `tools install` installs OAT skills/agents/templates/scripts by pack (`core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`) and auto-syncs provider views. The core pack (diagnostics, passive docs access) always installs at user scope. - `tools outdated` shows which installed tools have available updates. - `tools update --all` updates all outdated tools to bundled versions. - Use `--no-sync` on any mutation command to skip auto-sync. diff --git a/apps/oat-docs/docs/guide/cli-reference.md b/apps/oat-docs/docs/guide/cli-reference.md index 70cdf3d29..db374e9ed 100644 --- a/apps/oat-docs/docs/guide/cli-reference.md +++ b/apps/oat-docs/docs/guide/cli-reference.md @@ -55,7 +55,7 @@ Use the `oat tools` group to manage bundled OAT assets: - `oat tools list` - list installed tools, versions, pack membership, and update state - `oat tools info ` - inspect one installed skill or agent - `oat tools outdated` - show only assets with available updates -- `oat tools install` - install bundled packs such as `core`, `ideas`, `workflows`, `utility`, `project-management`, or `research` +- `oat tools install` - install bundled packs such as `core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, or `research` - `oat tools update` - update a named tool, a whole pack, or everything - `oat tools remove` - remove installed assets diff --git a/apps/oat-docs/docs/guide/documentation/quickstart.md b/apps/oat-docs/docs/guide/documentation/quickstart.md index 54d0f72b9..578a9571b 100644 --- a/apps/oat-docs/docs/guide/documentation/quickstart.md +++ b/apps/oat-docs/docs/guide/documentation/quickstart.md @@ -15,7 +15,7 @@ If you are developing inside the OAT repo itself, replace `oat ...` with - a docs app scaffolded with OAT defaults (Fumadocs or MkDocs) - `index.md`-driven navigation -- docs analysis and apply skills installed via the utility pack +- docs analysis and apply skills installed via the docs pack - a repeatable workflow for finding gaps, verifying claims, and applying docs changes ## 1. Initialize OAT in the repo @@ -28,19 +28,27 @@ This sets up the base OAT structure used by the CLI and installed tool packs. ## 2. Install the docs workflow skills -Fastest direct path: +Preferred direct path: ```bash -oat init tools utility +oat tools install docs ``` Interactive path: ```bash -oat init tools +oat tools install ``` -The utility pack installs `oat-docs-analyze` and `oat-docs-apply`. +Legacy pack-specific path: + +```bash +oat init tools docs +``` + +The docs pack installs `oat-docs-analyze`, `oat-docs-apply`, +`oat-agent-instructions-analyze`, and `oat-agent-instructions-apply`. For this +quickstart, the docs pair is the part you need immediately. ## 3. Scaffold the docs app @@ -140,7 +148,7 @@ Important: ## Typical loop 1. `oat init --scope project` -2. `oat init tools utility` +2. `oat tools install docs` 3. `oat docs init --app-name my-docs` 4. (optional) `oat docs migrate --docs-dir docs --config mkdocs.yml --apply` 5. Author docs with `index.md` + `## Contents` diff --git a/apps/oat-docs/docs/guide/documentation/workflows.md b/apps/oat-docs/docs/guide/documentation/workflows.md index fad393727..20ed46b60 100644 --- a/apps/oat-docs/docs/guide/documentation/workflows.md +++ b/apps/oat-docs/docs/guide/documentation/workflows.md @@ -8,6 +8,9 @@ description: 'Docs CLI helpers and skills for analysis and controlled documentat OAT’s docs workflow combines deterministic CLI helpers with higher-judgment skills for analysis and controlled updates. +Install the workflow skills with `oat tools install docs` (preferred) or +`oat init tools docs` before using the analyze/apply flow in a new repo. + ## Docs workflow pieces ### CLI helpers diff --git a/apps/oat-docs/docs/guide/getting-started.md b/apps/oat-docs/docs/guide/getting-started.md index 96afab757..802ce6a18 100644 --- a/apps/oat-docs/docs/guide/getting-started.md +++ b/apps/oat-docs/docs/guide/getting-started.md @@ -36,7 +36,7 @@ After core initialization completes, `oat init` can enter an interactive guided **Steps (each independently skippable):** -1. **Tool packs** — install OAT tool packs. The core pack (diagnostics, docs) is checked by default and always installs at user scope. Other packs (ideas, workflows, utility, research) install at project scope. +1. **Tool packs** — install OAT tool packs. The core pack (diagnostics, passive docs access) is checked by default and always installs at user scope. Other packs (`docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`) install at project scope. 2. **Local paths** — multi-select from default gitignored artifact paths (analysis, PR, reviews, ideas). Pre-existing paths are pre-checked; only new paths are added. 3. **Provider sync** — sync provider project views via `oat sync --scope project`. 4. **Summary** — reports what was configured: active providers, tool packs status, local paths added/existing, and provider sync status. Includes suggested next steps. diff --git a/apps/oat-docs/docs/guide/tool-packs.md b/apps/oat-docs/docs/guide/tool-packs.md index 531fe9750..f936be5aa 100644 --- a/apps/oat-docs/docs/guide/tool-packs.md +++ b/apps/oat-docs/docs/guide/tool-packs.md @@ -10,9 +10,10 @@ This page covers CLI commands that manage bundled OAT tool packs and installed O ## Bundled packs at a glance - `core` - foundational diagnostics and docs access (`oat-doctor`, `oat-docs`) +- `docs` - docs and agent-instructions governance workflows - `workflows` - project lifecycle skills, reviewer agents, and core project templates - `ideas` - lightweight ideation and promotion flows -- `utility` - standalone docs, review, and repo-maintenance helpers +- `utility` - review and repo-maintenance helpers - `project-management` - file-backed backlog/reference skills plus backlog and roadmap templates - `research` - research, analysis, comparison, and synthesis skills @@ -29,7 +30,7 @@ Purpose: Key behavior: - Scans installed skills and agents across project and user scopes -- Displays version, pack (`core`, `ideas`, `workflows`, `utility`, `project-management`, `research`, `custom`), and status (`current`, `outdated`, `newer`, `not-bundled`) +- Displays version, pack (`core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`, `custom`), and status (`current`, `outdated`, `newer`, `not-bundled`) - Supports `--scope` filtering and `--json` output ### `oat tools outdated` @@ -60,12 +61,12 @@ Key behavior: Purpose: -- Install bundled OAT tool packs (`core`, `ideas`, `workflows`, `utility`, `project-management`, `research`) +- Install bundled OAT tool packs (`core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`) Key behavior: - Same pack selection and install flow as `oat init tools` -- Pack-oriented install subcommands: `core`, `ideas`, `workflows`, `utility`, `project-management`, `research` +- Pack-oriented install subcommands: `core`, `docs`, `ideas`, `workflows`, `utility`, `project-management`, `research` - Tracks installed vs bundled skill versions and reports outdated skills - Interactive runs can prompt to update selected outdated skills - Auto-sync runs automatically after successful install (provider views are updated) @@ -112,6 +113,30 @@ Key behavior: - Installation also bundles OAT documentation to `~/.oat/docs/` for the oat-docs skill. - `oat tools update --pack core` refreshes both skills and `~/.oat/docs/` documentation. +## Docs pack + +The `docs` pack contains active documentation and instruction-governance +workflows: + +- **oat-docs-analyze** — Analyze a docs surface for contract coverage, nav + drift, stale claims, and coverage gaps. +- **oat-docs-apply** — Apply only approved, evidence-backed docs-analysis + recommendations. +- **oat-agent-instructions-analyze** — Evaluate `AGENTS.md` and provider + instruction coverage, quality, and drift. +- **oat-agent-instructions-apply** — Generate or update approved instruction + files from an analysis artifact. + +Key behavior: + +- Docs pack installs at the selected scope, typically `project`. +- It complements the `core` pack: `oat-docs` answers questions from bundled + docs, while the `docs` pack adds analyze/apply workflows. +- `oat tools install docs` is the preferred install path; `oat init tools docs` + remains available for backward compatibility. +- `oat tools update --pack docs` and `oat tools remove --pack docs` manage the + workflow skills as a unit. + ### Auto-sync behavior All mutation commands (`install`, `update`, `remove`) automatically run `oat sync --scope ` after successful operations. This ensures provider views stay in sync with canonical assets without manual intervention. diff --git a/apps/oat-docs/docs/quickstart.md b/apps/oat-docs/docs/quickstart.md index 575545b93..1d4ded1e3 100644 --- a/apps/oat-docs/docs/quickstart.md +++ b/apps/oat-docs/docs/quickstart.md @@ -46,7 +46,7 @@ pnpm run cli -- tools install project-management Notes: -- Installs OAT tool packs (`ideas`, `workflows`, `utility`, `project-management`, `research`) into canonical directories. +- Installs OAT tool packs (`docs`, `ideas`, `workflows`, `utility`, `project-management`, `research`) into canonical directories. The `core` pack is always installed at user scope for diagnostics and passive docs access. - `oat init tools` remains available as a backward-compatible install path. - If installed OAT skills are older than bundled versions, interactive runs prompt for selective updates. - Non-interactive runs report outdated skills without updating them. diff --git a/packages/cli/scripts/bundle-assets.sh b/packages/cli/scripts/bundle-assets.sh index 8bddb1a63..87c55fdb1 100755 --- a/packages/cli/scripts/bundle-assets.sh +++ b/packages/cli/scripts/bundle-assets.sh @@ -80,7 +80,7 @@ if [ -d "${REPO_ROOT}/apps/oat-docs/docs" ]; then cp -R "${REPO_ROOT}/apps/oat-docs/docs/." "${ASSETS}/docs/" fi -for script in generate-oat-state.sh generate-thin-index.sh; do +for script in generate-oat-state.sh generate-thin-index.sh resolve-tracking.sh; do SOURCE_SCRIPT="${REPO_ROOT}/.oat/scripts/${script}" if [ -f "${SOURCE_SCRIPT}" ]; then cp "${SOURCE_SCRIPT}" "${ASSETS}/scripts/" diff --git a/packages/cli/src/commands/help-snapshots.test.ts b/packages/cli/src/commands/help-snapshots.test.ts index d8fd19492..bcebcd550 100644 --- a/packages/cli/src/commands/help-snapshots.test.ts +++ b/packages/cli/src/commands/help-snapshots.test.ts @@ -78,7 +78,7 @@ describe('help output snapshots', () => { -h, --help display help for command Commands: - tools Install OAT tool packs (core, ideas, workflows, utility, + tools Install OAT tool packs (core, ideas, docs, workflows, utility, project-management, research) " `); @@ -287,7 +287,7 @@ describe('help output snapshots', () => { Remove installed skills by pack Options: - --pack Skill pack to remove (ideas|workflows|utility|research) + --pack Skill pack to remove (ideas|docs|workflows|utility|research) --dry-run Preview removal without applying -h, --help display help for command " @@ -597,8 +597,8 @@ describe('help output snapshots', () => { info Show details for an installed tool update [options] [name] Update installed tools to bundled versions remove [options] [name] Remove installed tools - install [options] Install OAT tool packs (core, ideas, workflows, - utility, project-management, research) + install [options] Install OAT tool packs (core, ideas, docs, + workflows, utility, project-management, research) help [command] display help for command " `); @@ -644,8 +644,8 @@ describe('help output snapshots', () => { expect(help).toMatchInlineSnapshot(` "Usage: oat tools install [options] [command] - Install OAT tool packs (core, ideas, workflows, utility, project-management, - research) + Install OAT tool packs (core, ideas, docs, workflows, utility, + project-management, research) Options: --no-sync Skip auto-sync after install @@ -655,6 +655,7 @@ describe('help output snapshots', () => { core [options] Install OAT core skills (diagnostics, docs) ideas [options] Install OAT ideas skills, templates, and idea workflow files + docs [options] Install OAT docs workflow skills project-management [options] Install OAT project-management skills and templates workflows [options] Install OAT workflows skills, agents, @@ -681,7 +682,7 @@ describe('help output snapshots', () => { Options: --pack Remove all tools in a pack - (core|ideas|workflows|utility|project-management|research) + (core|ideas|docs|workflows|utility|project-management|research) --all Remove all installed tools --dry-run Preview removals without applying --no-sync Skip auto-sync after removal @@ -706,7 +707,7 @@ describe('help output snapshots', () => { Options: --pack Update all tools in a pack - (core|ideas|workflows|utility|project-management|research) + (core|ideas|docs|workflows|utility|project-management|research) --all Update all outdated tools --dry-run Preview updates without applying --no-sync Skip auto-sync after update diff --git a/packages/cli/src/commands/init/tools/docs/index.test.ts b/packages/cli/src/commands/init/tools/docs/index.test.ts new file mode 100644 index 000000000..6b8797922 --- /dev/null +++ b/packages/cli/src/commands/init/tools/docs/index.test.ts @@ -0,0 +1,154 @@ +import type { CommandContext, GlobalOptions } from '@app/command-context'; +import { + createLoggerCapture, + type LoggerCapture, +} from '@commands/__tests__/helpers'; +import type { MultiSelectChoice } from '@commands/shared/shared.prompts'; +import type { Scope } from '@shared/types'; +import { Command } from 'commander'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createInitToolsDocsCommand } from './index'; +import { DOCS_SKILLS } from './install-docs'; + +interface HarnessOptions { + scope?: Scope; + interactive?: boolean; + selectResponses?: Array; + confirmResponses?: boolean[]; +} + +function createHarness(options: HarnessOptions = {}): { + capture: LoggerCapture; + command: Command; + selectManyWithAbort: ReturnType; + confirmAction: ReturnType; + installDocs: ReturnType; +} { + const capture = createLoggerCapture(); + const selectResponses = [ + ...(options.selectResponses ?? [['oat-docs-analyze']]), + ]; + const confirmResponses = [...(options.confirmResponses ?? [true])]; + + const selectManyWithAbort = vi.fn( + async (_message: string, _choices: MultiSelectChoice[]) => + selectResponses.shift() ?? ['oat-docs-analyze'], + ); + const confirmAction = vi.fn(async () => confirmResponses.shift() ?? true); + const installDocs = vi.fn(async () => ({ + copiedSkills: ['oat-docs-analyze'], + updatedSkills: [], + skippedSkills: [], + outdatedSkills: [], + copiedScripts: ['resolve-tracking.sh'], + updatedScripts: [], + skippedScripts: [], + })); + + const command = createInitToolsDocsCommand({ + buildCommandContext: (globalOptions: GlobalOptions): CommandContext => ({ + scope: (globalOptions.scope ?? options.scope ?? 'all') as Scope, + dryRun: false, + verbose: globalOptions.verbose ?? false, + json: globalOptions.json ?? false, + cwd: globalOptions.cwd ?? '/tmp/workspace', + home: '/tmp/home', + interactive: options.interactive ?? !(globalOptions.json ?? false), + logger: capture.logger, + }), + resolveProjectRoot: vi.fn(async () => '/tmp/workspace'), + resolveScopeRoot: vi.fn((_scope: 'project' | 'user', _cwd, home) => home), + resolveAssetsRoot: vi.fn(async () => '/tmp/assets'), + installDocs, + selectManyWithAbort, + confirmAction, + }); + + return { + capture, + command, + selectManyWithAbort, + confirmAction, + installDocs, + }; +} + +async function runCommand( + command: Command, + args: string[] = [], + globalArgs: string[] = [], +): Promise { + const program = new Command() + .name('oat') + .option('--json') + .option('--verbose') + .option('--scope ') + .option('--cwd ') + .exitOverride(); + + const init = new Command('init'); + const tools = new Command('tools'); + tools.addCommand(command); + init.addCommand(tools); + program.addCommand(init); + + await program.parseAsync([...globalArgs, 'init', 'tools', 'docs', ...args], { + from: 'user', + }); +} + +describe('createInitToolsDocsCommand', () => { + let originalExitCode: number | undefined; + + beforeEach(() => { + originalExitCode = process.exitCode; + process.exitCode = undefined; + }); + + afterEach(() => { + process.exitCode = originalExitCode; + }); + + it('interactive mode shows multi-select with all checked', async () => { + const { command, selectManyWithAbort } = createHarness({ + interactive: true, + }); + + await runCommand(command, [], ['--scope', 'project']); + + expect(selectManyWithAbort).toHaveBeenCalledTimes(1); + const choices = selectManyWithAbort.mock.calls[0]?.[1] as Array<{ + value: string; + checked?: boolean; + }>; + expect( + choices.find((choice) => choice.value === 'oat-docs-analyze')?.checked, + ).toBe(true); + }); + + it('non-interactive installs all docs skills', async () => { + const { command, selectManyWithAbort, installDocs } = createHarness({ + interactive: false, + }); + + await runCommand(command, [], ['--scope', 'project']); + + expect(selectManyWithAbort).not.toHaveBeenCalled(); + expect(installDocs).toHaveBeenCalledWith( + expect.objectContaining({ + skills: [...DOCS_SKILLS], + }), + ); + }); + + it('--scope user works', async () => { + const { command, installDocs } = createHarness({ interactive: false }); + + await runCommand(command, [], ['--scope', 'user']); + + expect(installDocs).toHaveBeenCalledWith( + expect.objectContaining({ targetRoot: '/tmp/home' }), + ); + }); +}); diff --git a/packages/cli/src/commands/init/tools/docs/index.ts b/packages/cli/src/commands/init/tools/docs/index.ts new file mode 100644 index 000000000..c5e98dbaa --- /dev/null +++ b/packages/cli/src/commands/init/tools/docs/index.ts @@ -0,0 +1,192 @@ +import { + buildCommandContext, + type CommandContext, + type GlobalOptions, +} from '@app/command-context'; +import { + confirmAction, + type MultiSelectChoice, + type PromptContext, + selectManyWithAbort, +} from '@commands/shared/shared.prompts'; +import { readGlobalOptions } from '@commands/shared/shared.utils'; +import { resolveAssetsRoot } from '@fs/assets'; +import { resolveProjectRoot, resolveScopeRoot } from '@fs/paths'; +import { Command } from 'commander'; + +import { + DOCS_SKILLS, + installDocs as defaultInstallDocs, + type InstallDocsOptions, + type InstallDocsResult, +} from './install-docs'; + +interface InitToolsDocsOptions { + force?: boolean; +} + +type DocsScope = 'project' | 'user'; + +interface InitToolsDocsDependencies { + buildCommandContext: (options: GlobalOptions) => CommandContext; + resolveProjectRoot: (cwd: string) => Promise; + resolveScopeRoot: (scope: DocsScope, cwd: string, home: string) => string; + resolveAssetsRoot: () => Promise; + installDocs: (options: InstallDocsOptions) => Promise; + selectManyWithAbort: ( + message: string, + choices: MultiSelectChoice[], + ctx: PromptContext, + ) => Promise; + confirmAction: (message: string, ctx: PromptContext) => Promise; +} + +const DEFAULT_DEPENDENCIES: InitToolsDocsDependencies = { + buildCommandContext, + resolveProjectRoot, + resolveScopeRoot, + resolveAssetsRoot, + installDocs: defaultInstallDocs, + selectManyWithAbort, + confirmAction, +}; + +function resolveScope(context: CommandContext): DocsScope { + return context.scope === 'user' ? 'user' : 'project'; +} + +function reportSuccess( + context: CommandContext, + scope: DocsScope, + targetRoot: string, + assetsRoot: string, + selectedSkills: string[], + result: InstallDocsResult, +): void { + if (context.json) { + context.logger.json({ + status: 'ok', + scope, + targetRoot, + assetsRoot, + selectedSkills, + result, + }); + return; + } + + context.logger.info('Installed docs tool pack.'); + context.logger.info(`Scope: ${scope}`); + context.logger.info(`Target root: ${targetRoot}`); + context.logger.info( + `Selected skills: ${selectedSkills.join(', ') || '(none)'}`, + ); + context.logger.info( + `Skills: copied=${result.copiedSkills.length}, updated=${result.updatedSkills.length}, skipped=${result.skippedSkills.length}`, + ); + context.logger.info( + `Scripts: copied=${result.copiedScripts.length}, updated=${result.updatedScripts.length}, skipped=${result.skippedScripts.length}`, + ); + context.logger.info(`Run: oat sync --scope ${scope}`); +} + +async function runInitToolsDocs( + context: CommandContext, + options: InitToolsDocsOptions, + dependencies: InitToolsDocsDependencies, +): Promise { + try { + const scope = resolveScope(context); + const targetRoot = + scope === 'project' + ? await dependencies.resolveProjectRoot(context.cwd) + : dependencies.resolveScopeRoot('user', context.cwd, context.home); + + const selectedSkills = context.interactive + ? await dependencies.selectManyWithAbort( + 'Select docs skills to install', + DOCS_SKILLS.map((skill) => ({ + label: skill, + value: skill, + checked: true, + })), + { interactive: context.interactive }, + ) + : [...DOCS_SKILLS]; + + if (selectedSkills === null) { + if (!context.json) { + context.logger.info('Cancelled: no docs skills installed.'); + } + process.exitCode = 0; + return; + } + + if (selectedSkills.length === 0) { + if (!context.json) { + context.logger.info('No docs skills selected.'); + } + process.exitCode = 0; + return; + } + + if (options.force && context.interactive) { + const confirmed = await dependencies.confirmAction( + `Force overwrite existing docs assets in ${scope} scope?`, + { interactive: context.interactive }, + ); + if (!confirmed) { + if (!context.json) { + context.logger.info('Cancelled: no files were overwritten.'); + } + process.exitCode = 0; + return; + } + } + + const assetsRoot = await dependencies.resolveAssetsRoot(); + const result = await dependencies.installDocs({ + assetsRoot, + targetRoot, + skills: selectedSkills, + force: options.force, + }); + + reportSuccess( + context, + scope, + targetRoot, + assetsRoot, + selectedSkills, + result, + ); + process.exitCode = 0; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (context.json) { + context.logger.json({ status: 'error', message }); + } else { + context.logger.error(message); + } + process.exitCode = 1; + } +} + +export function createInitToolsDocsCommand( + overrides: Partial = {}, +): Command { + const dependencies: InitToolsDocsDependencies = { + ...DEFAULT_DEPENDENCIES, + ...overrides, + }; + + return new Command('docs') + .description('Install OAT docs workflow skills') + .option('--force', 'Overwrite existing files where applicable') + .action(async (options: InitToolsDocsOptions, command: Command) => { + const context = dependencies.buildCommandContext( + readGlobalOptions(command), + ); + await runInitToolsDocs(context, options, dependencies); + }); +} diff --git a/packages/cli/src/commands/init/tools/docs/install-docs.test.ts b/packages/cli/src/commands/init/tools/docs/install-docs.test.ts new file mode 100644 index 000000000..af5aeb399 --- /dev/null +++ b/packages/cli/src/commands/init/tools/docs/install-docs.test.ts @@ -0,0 +1,129 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { installDocs } from './install-docs'; + +const tempDirs: string[] = []; + +async function makeTempDir(): Promise { + const dir = await mkdtemp(join(tmpdir(), 'oat-docs-pack-')); + tempDirs.push(dir); + return dir; +} + +async function seedAssets(assetsRoot: string): Promise { + await mkdir(join(assetsRoot, 'skills', 'oat-docs-analyze'), { + recursive: true, + }); + await mkdir(join(assetsRoot, 'skills', 'oat-docs-apply'), { + recursive: true, + }); + await mkdir(join(assetsRoot, 'scripts'), { recursive: true }); + await writeFile( + join(assetsRoot, 'skills', 'oat-docs-analyze', 'SKILL.md'), + '---\nname: oat-docs-analyze\nversion: 1.0.0\n---\n', + 'utf8', + ); + await writeFile( + join(assetsRoot, 'skills', 'oat-docs-apply', 'SKILL.md'), + '---\nname: oat-docs-apply\nversion: 1.0.0\n---\n', + 'utf8', + ); + await writeFile( + join(assetsRoot, 'scripts', 'resolve-tracking.sh'), + '#!/bin/sh\necho tracking\n', + 'utf8', + ); +} + +describe('installDocs', () => { + afterEach(async () => { + await Promise.all( + tempDirs.map(async (dir) => { + await rm(dir, { recursive: true, force: true }); + }), + ); + tempDirs.length = 0; + }); + + it('copies selected docs skills at project scope', async () => { + const root = await makeTempDir(); + const assetsRoot = join(root, 'assets'); + const targetRoot = join(root, 'project-target'); + await seedAssets(assetsRoot); + + const result = await installDocs({ + assetsRoot, + targetRoot, + skills: ['oat-docs-analyze'], + }); + + expect(result.copiedSkills).toEqual(['oat-docs-analyze']); + expect(result.copiedScripts).toEqual(['resolve-tracking.sh']); + expect(result.outdatedSkills).toEqual([]); + await expect( + readFile( + join(targetRoot, '.agents', 'skills', 'oat-docs-analyze', 'SKILL.md'), + 'utf8', + ), + ).resolves.toContain('oat-docs-analyze'); + }); + + it('copies selected docs skills at user scope', async () => { + const root = await makeTempDir(); + const assetsRoot = join(root, 'assets'); + const targetRoot = join(root, 'user-target'); + await seedAssets(assetsRoot); + + const result = await installDocs({ + assetsRoot, + targetRoot, + skills: ['oat-docs-apply'], + }); + + expect(result.copiedSkills).toEqual(['oat-docs-apply']); + expect(result.copiedScripts).toEqual(['resolve-tracking.sh']); + expect(result.outdatedSkills).toEqual([]); + await expect( + readFile( + join(targetRoot, '.agents', 'skills', 'oat-docs-apply', 'SKILL.md'), + 'utf8', + ), + ).resolves.toContain('oat-docs-apply'); + }); + + it('tracks outdated docs skills when bundled version is newer', async () => { + const root = await makeTempDir(); + const assetsRoot = join(root, 'assets'); + const targetRoot = join(root, 'target'); + await seedAssets(assetsRoot); + await installDocs({ + assetsRoot, + targetRoot, + skills: ['oat-docs-analyze'], + }); + + await writeFile( + join(assetsRoot, 'skills', 'oat-docs-analyze', 'SKILL.md'), + '---\nname: oat-docs-analyze\nversion: 1.1.0\n---\n', + 'utf8', + ); + + const result = await installDocs({ + assetsRoot, + targetRoot, + skills: ['oat-docs-analyze'], + }); + + expect(result.copiedSkills).toEqual([]); + expect(result.updatedSkills).toEqual([]); + expect(result.skippedSkills).toEqual([]); + expect(result.skippedScripts).toEqual(['resolve-tracking.sh']); + expect(result.outdatedSkills).toEqual([ + { name: 'oat-docs-analyze', installed: '1.0.0', bundled: '1.1.0' }, + ]); + }); +}); diff --git a/packages/cli/src/commands/init/tools/docs/install-docs.ts b/packages/cli/src/commands/init/tools/docs/install-docs.ts new file mode 100644 index 000000000..a1c966f91 --- /dev/null +++ b/packages/cli/src/commands/init/tools/docs/install-docs.ts @@ -0,0 +1,96 @@ +import { join } from 'node:path'; + +import { + copyDirWithVersionCheck, + copyFileWithStatus, +} from '@commands/init/tools/shared/copy-helpers'; +import { + DOCS_SCRIPTS, + DOCS_SKILLS, +} from '@commands/init/tools/shared/skill-manifest'; +import { fileExists } from '@fs/io'; + +export { DOCS_SCRIPTS, DOCS_SKILLS }; + +export interface InstallDocsOptions { + assetsRoot: string; + targetRoot: string; + skills: string[]; + force?: boolean; +} + +export interface InstallDocsResult { + copiedSkills: string[]; + updatedSkills: string[]; + skippedSkills: string[]; + outdatedSkills: Array<{ + name: string; + installed: string | null; + bundled: string | null; + }>; + copiedScripts: string[]; + updatedScripts: string[]; + skippedScripts: string[]; +} + +export async function installDocs( + options: InstallDocsOptions, +): Promise { + const force = options.force ?? false; + const result: InstallDocsResult = { + copiedSkills: [], + updatedSkills: [], + skippedSkills: [], + outdatedSkills: [], + copiedScripts: [], + updatedScripts: [], + skippedScripts: [], + }; + + for (const skill of options.skills) { + const source = join(options.assetsRoot, 'skills', skill); + const destination = join(options.targetRoot, '.agents', 'skills', skill); + const resultWithVersion = await copyDirWithVersionCheck( + source, + destination, + force, + ); + + if (resultWithVersion.status === 'copied') { + result.copiedSkills.push(skill); + } else if (resultWithVersion.status === 'updated') { + result.updatedSkills.push(skill); + } else if (resultWithVersion.status === 'outdated') { + result.outdatedSkills.push({ + name: skill, + installed: resultWithVersion.installedVersion ?? null, + bundled: resultWithVersion.bundledVersion ?? null, + }); + } else { + result.skippedSkills.push(skill); + } + } + + for (const script of DOCS_SCRIPTS) { + const source = join(options.assetsRoot, 'scripts', script); + const destination = join(options.targetRoot, '.oat', 'scripts', script); + const sourceExists = await fileExists(source); + + if (!sourceExists) { + result.skippedScripts.push(script); + continue; + } + + const copyStatus = await copyFileWithStatus(source, destination, force); + + if (copyStatus === 'copied') { + result.copiedScripts.push(script); + } else if (copyStatus === 'updated') { + result.updatedScripts.push(script); + } else { + result.skippedScripts.push(script); + } + } + + return result; +} diff --git a/packages/cli/src/commands/init/tools/index.test.ts b/packages/cli/src/commands/init/tools/index.test.ts index 995ea931f..9f2a4870d 100644 --- a/packages/cli/src/commands/init/tools/index.test.ts +++ b/packages/cli/src/commands/init/tools/index.test.ts @@ -21,7 +21,7 @@ function createHarness(options: HarnessOptions = {}) { const capture = createLoggerCapture(); const packSelection = [ ...(options.packSelection ?? [ - ['core', 'ideas', 'workflows', 'utility', 'research'], + ['core', 'ideas', 'docs', 'workflows', 'utility', 'research'], ]), ]; const scopeSelection = [...(options.scopeSelection ?? ['project'])]; @@ -30,7 +30,7 @@ function createHarness(options: HarnessOptions = {}) { async (_message: string, _choices: MultiSelectChoice[]) => { const next = packSelection.shift(); return next === undefined - ? ['core', 'ideas', 'workflows', 'utility', 'research'] + ? ['core', 'ideas', 'docs', 'workflows', 'utility', 'research'] : next; }, ); @@ -48,6 +48,12 @@ function createHarness(options: HarnessOptions = {}) { outdatedSkills: [], docsStatus: 'copied' as const, })); + const installDocs = vi.fn(async () => ({ + copiedSkills: ['oat-docs-analyze'], + updatedSkills: [], + skippedSkills: [], + outdatedSkills: [], + })); const installIdeas = vi.fn(async () => ({ copiedSkills: ['oat-idea-new'], updatedSkills: [], @@ -138,6 +144,7 @@ function createHarness(options: HarnessOptions = {}) { selectManyWithAbort, selectWithAbort, installCore, + installDocs, installIdeas, installWorkflows, installUtility, @@ -158,6 +165,7 @@ function createHarness(options: HarnessOptions = {}) { selectManyWithAbort, selectWithAbort, installCore, + installDocs, installIdeas, installWorkflows, installUtility, @@ -206,11 +214,12 @@ describe('createInitToolsCommand', () => { process.exitCode = originalExitCode; }); - it('registers core, ideas, project-management, workflows, utility, and research subcommands', () => { + it('registers core, ideas, docs, project-management, workflows, utility, and research subcommands', () => { const { command } = createHarness(); const subcommands = command.commands.map((subcommand) => subcommand.name()); expect(subcommands).toContain('core'); expect(subcommands).toContain('ideas'); + expect(subcommands).toContain('docs'); expect(subcommands).toContain('project-management'); expect(subcommands).toContain('workflows'); expect(subcommands).toContain('utility'); @@ -234,6 +243,9 @@ describe('createInitToolsCommand', () => { expect( choices.some((choice) => choice.label.includes('[project|user]')), ).toBe(true); + expect(choices.find((choice) => choice.value === 'docs')?.checked).toBe( + true, + ); expect( choices.find((choice) => choice.value === 'project-management')?.checked, ).toBe(false); @@ -248,6 +260,7 @@ describe('createInitToolsCommand', () => { const { command, installCore, + installDocs, installIdeas, installWorkflows, installUtility, @@ -264,6 +277,9 @@ describe('createInitToolsCommand', () => { expect(installIdeas).toHaveBeenCalledWith( expect.objectContaining({ targetRoot: '/tmp/workspace' }), ); + expect(installDocs).toHaveBeenCalledWith( + expect.objectContaining({ targetRoot: '/tmp/workspace' }), + ); expect(installWorkflows).toHaveBeenCalledWith( expect.objectContaining({ targetRoot: '/tmp/workspace' }), ); diff --git a/packages/cli/src/commands/init/tools/index.ts b/packages/cli/src/commands/init/tools/index.ts index b18005591..2f04c603f 100644 --- a/packages/cli/src/commands/init/tools/index.ts +++ b/packages/cli/src/commands/init/tools/index.ts @@ -36,6 +36,13 @@ import { type InstallCoreOptions, type InstallCoreResult, } from './core/install-core'; +import { createInitToolsDocsCommand } from './docs'; +import { + DOCS_SKILLS, + installDocs as defaultInstallDocs, + type InstallDocsOptions, + type InstallDocsResult, +} from './docs/install-docs'; import { createInitToolsIdeasCommand } from './ideas'; import { installIdeas as defaultInstallIdeas, @@ -73,6 +80,7 @@ type InstallScope = 'project' | 'user'; export type ToolPack = | 'core' | 'ideas' + | 'docs' | 'workflows' | 'utility' | 'project-management' @@ -94,6 +102,7 @@ interface InitToolsDependencies { ctx: PromptContext, ) => Promise; installCore: (options: InstallCoreOptions) => Promise; + installDocs: (options: InstallDocsOptions) => Promise; installIdeas: (options: InstallIdeasOptions) => Promise; installWorkflows: ( options: InstallWorkflowsOptions, @@ -144,6 +153,7 @@ function formatVersionForDisplay(version: string | null): string { const PACK_CHOICES: MultiSelectChoice[] = [ { label: 'Core [user]', value: 'core', checked: true }, { label: 'Ideas [project|user]', value: 'ideas', checked: true }, + { label: 'Docs [project|user]', value: 'docs', checked: true }, { label: 'Project Management [project]', value: 'project-management', @@ -162,6 +172,7 @@ const DEFAULT_DEPENDENCIES: InitToolsDependencies = { selectManyWithAbort, selectWithAbort, installCore: defaultInstallCore, + installDocs: defaultInstallDocs, installIdeas: defaultInstallIdeas, installWorkflows: defaultInstallWorkflows, installUtility: defaultInstallUtility, @@ -178,6 +189,7 @@ const DEFAULT_DEPENDENCIES: InitToolsDependencies = { const USER_ELIGIBLE_PACKS: ReadonlySet = new Set([ 'ideas', + 'docs', 'utility', 'research', ]); @@ -311,12 +323,14 @@ async function updateOutdatedSkills( const PACK_DESCRIPTIONS: Record = { core: 'Diagnostics and documentation (oat-doctor, oat-docs)', + docs: 'Documentation and instruction governance workflows', workflows: 'Project lifecycle (create, discover, plan, implement, review, complete)', ideas: 'Idea capture and refinement', 'project-management': 'Local backlog, roadmap, and reference doc management (oat-pjm-* skills)', - utility: 'Standalone utilities (reviews, docs analysis, agent instructions)', + utility: + 'Standalone utilities (skill authoring, maintainability review, code reviews)', research: 'Research, analysis, verification, and synthesis', }; @@ -377,7 +391,7 @@ export async function runInitTools( PACK_CHOICES, { interactive: context.interactive }, )) ?? []) - : ['core', 'ideas', 'workflows', 'utility', 'research']; + : ['core', 'ideas', 'docs', 'workflows', 'utility', 'research']; if (!context.interactive) { selectedPacks.push('project-management'); @@ -431,6 +445,18 @@ export async function runInitTools( } } + if (selectedPacks.includes('docs')) { + const targetRoot = packRoot('docs'); + const docsResult = await dependencies.installDocs({ + assetsRoot, + targetRoot, + skills: [...DOCS_SKILLS], + }); + for (const skill of docsResult.outdatedSkills) { + outdatedSkills.push({ ...skill, targetRoot }); + } + } + if (selectedPacks.includes('workflows')) { const workflowsResult = await dependencies.installWorkflows({ assetsRoot, @@ -621,10 +647,11 @@ export function createInitToolsCommand( return new Command('tools') .description( - 'Install OAT tool packs (core, ideas, workflows, utility, project-management, research)', + 'Install OAT tool packs (core, ideas, docs, workflows, utility, project-management, research)', ) .addCommand(createInitToolsCoreCommand()) .addCommand(createInitToolsIdeasCommand()) + .addCommand(createInitToolsDocsCommand()) .addCommand(createInitToolsProjectManagementCommand()) .addCommand(createInitToolsWorkflowsCommand()) .addCommand(createInitToolsUtilityCommand()) diff --git a/packages/cli/src/commands/init/tools/shared/bundle-consistency.test.ts b/packages/cli/src/commands/init/tools/shared/bundle-consistency.test.ts index 291d211da..baed4165c 100644 --- a/packages/cli/src/commands/init/tools/shared/bundle-consistency.test.ts +++ b/packages/cli/src/commands/init/tools/shared/bundle-consistency.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { CORE_SKILLS } from '../core/install-core'; +import { DOCS_SKILLS } from '../docs/install-docs'; import { IDEA_SKILLS } from '../ideas/install-ideas'; import { PROJECT_MANAGEMENT_SKILLS } from '../project-management/install-project-management'; import { RESEARCH_SKILLS } from '../research/install-research'; @@ -83,6 +84,16 @@ describe('bundle-assets.sh consistency', () => { ).toEqual([]); }); + it('bundles every docs skill', () => { + const missing = DOCS_SKILLS.filter( + (skill) => !bundleSkills.includes(skill), + ); + expect( + missing, + `Missing from bundle-assets.sh SKILLS array: ${missing.join(', ')}`, + ).toEqual([]); + }); + it('bundles every utility skill', () => { const missing = UTILITY_SKILLS.filter( (skill) => !bundleSkills.includes(skill), @@ -128,6 +139,7 @@ describe('bundle-assets.sh consistency', () => { ...CORE_SKILLS, ...WORKFLOW_SKILLS, ...IDEA_SKILLS, + ...DOCS_SKILLS, ...UTILITY_SKILLS, ...PROJECT_MANAGEMENT_SKILLS, ...RESEARCH_SKILLS, diff --git a/packages/cli/src/commands/init/tools/shared/skill-manifest.ts b/packages/cli/src/commands/init/tools/shared/skill-manifest.ts index fe40f3a02..67c882354 100644 --- a/packages/cli/src/commands/init/tools/shared/skill-manifest.ts +++ b/packages/cli/src/commands/init/tools/shared/skill-manifest.ts @@ -54,6 +54,7 @@ export const WORKFLOW_TEMPLATES = [ export const WORKFLOW_SCRIPTS = [ 'generate-oat-state.sh', 'generate-thin-index.sh', + 'resolve-tracking.sh', ] as const; // ── Ideas pack ───────────────────────────────────────────────────── @@ -69,14 +70,21 @@ export const IDEA_SKILLS = [ export const CORE_SKILLS = ['oat-docs', 'oat-doctor'] as const; -// ── Utility pack ─────────────────────────────────────────────────── +// ── Docs pack ───────────────────────────────────────────────────── -export const UTILITY_SKILLS = [ - 'create-agnostic-skill', +export const DOCS_SKILLS = [ 'oat-agent-instructions-analyze', 'oat-agent-instructions-apply', 'oat-docs-analyze', 'oat-docs-apply', +] as const; + +export const DOCS_SCRIPTS = ['resolve-tracking.sh'] as const; + +// ── Utility pack ─────────────────────────────────────────────────── + +export const UTILITY_SKILLS = [ + 'create-agnostic-skill', 'oat-repo-maintainability-review', 'oat-review-provide', 'oat-review-receive', diff --git a/packages/cli/src/commands/init/tools/workflows/install-workflows.test.ts b/packages/cli/src/commands/init/tools/workflows/install-workflows.test.ts index ab186aacf..2fdea5118 100644 --- a/packages/cli/src/commands/init/tools/workflows/install-workflows.test.ts +++ b/packages/cli/src/commands/init/tools/workflows/install-workflows.test.ts @@ -78,7 +78,7 @@ describe('installWorkflows', () => { tempDirs.length = 0; }); - it('copies all workflow skills, 2 agents, 6 templates, and 2 scripts on fresh install', async () => { + it('copies all workflow skills, 2 agents, 6 templates, and 3 scripts on fresh install', async () => { const root = await makeTempDir(); const assetsRoot = join(root, 'assets'); const targetRoot = join(root, 'target'); @@ -90,7 +90,7 @@ describe('installWorkflows', () => { expect(result.outdatedSkills).toEqual([]); expect(result.copiedAgents).toHaveLength(2); expect(result.copiedTemplates).toHaveLength(6); - expect(result.copiedScripts).toHaveLength(2); + expect(result.copiedScripts).toHaveLength(3); expect(result.projectsRootInitialized).toBe(true); }); @@ -250,7 +250,7 @@ describe('installWorkflows', () => { const result = await installWorkflows({ assetsRoot, targetRoot }); expect(result.copiedScripts).toEqual([]); - expect(result.skippedScripts).toHaveLength(2); + expect(result.skippedScripts).toHaveLength(3); }); it('skips all items on idempotent re-run', async () => { @@ -270,7 +270,7 @@ describe('installWorkflows', () => { expect(second.outdatedSkills).toEqual([]); expect(second.skippedAgents).toHaveLength(2); expect(second.skippedTemplates).toHaveLength(6); - expect(second.skippedScripts).toHaveLength(2); + expect(second.skippedScripts).toHaveLength(3); }); it('overwrites with force=true, tracking updated arrays', async () => { @@ -296,7 +296,7 @@ describe('installWorkflows', () => { expect(result.outdatedSkills).toEqual([]); expect(result.updatedAgents).toHaveLength(2); expect(result.updatedTemplates).toHaveLength(6); - expect(result.updatedScripts).toHaveLength(2); + expect(result.updatedScripts).toHaveLength(3); }); it('tracks outdated skills without overwriting when not forced', async () => { diff --git a/packages/cli/src/commands/remove/skills/remove-skills.test.ts b/packages/cli/src/commands/remove/skills/remove-skills.test.ts index c937349e6..4d3db14d3 100644 --- a/packages/cli/src/commands/remove/skills/remove-skills.test.ts +++ b/packages/cli/src/commands/remove/skills/remove-skills.test.ts @@ -3,6 +3,7 @@ import { createLoggerCapture, type LoggerCapture, } from '@commands/__tests__/helpers'; +import { DOCS_SKILLS } from '@commands/init/tools/docs/install-docs'; import { RESEARCH_SKILLS } from '@commands/init/tools/research/install-research'; import { UTILITY_SKILLS } from '@commands/init/tools/utility/install-utility'; import { WORKFLOW_SKILLS } from '@commands/init/tools/workflows/install-workflows'; @@ -103,6 +104,14 @@ describe('createRemoveSkillsCommand', () => { expect(process.exitCode).toBe(0); }); + it('runs remove-skill workflow for docs pack members', async () => { + const { command, runRemoveSkill } = createHarness({ interactive: false }); + await runCommand(command, [], ['--pack', 'docs']); + + expect(runRemoveSkill).toHaveBeenCalledTimes(DOCS_SKILLS.length); + expect(process.exitCode).toBe(0); + }); + it('asks for confirmation when interactive and pack has more than 3 skills', async () => { const { command, confirmAction, runRemoveSkill, capture } = createHarness({ interactive: true, diff --git a/packages/cli/src/commands/remove/skills/remove-skills.ts b/packages/cli/src/commands/remove/skills/remove-skills.ts index 387d32938..88d0e56df 100644 --- a/packages/cli/src/commands/remove/skills/remove-skills.ts +++ b/packages/cli/src/commands/remove/skills/remove-skills.ts @@ -1,4 +1,5 @@ import { buildCommandContext, type CommandContext } from '@app/command-context'; +import { DOCS_SKILLS } from '@commands/init/tools/docs/install-docs'; import { IDEA_SKILLS } from '@commands/init/tools/ideas/install-ideas'; import { RESEARCH_SKILLS } from '@commands/init/tools/research/install-research'; import { UTILITY_SKILLS } from '@commands/init/tools/utility/install-utility'; @@ -21,10 +22,11 @@ interface RemoveSkillsOptions { dryRun?: boolean; } -type PackName = 'ideas' | 'workflows' | 'utility' | 'research'; +type PackName = 'ideas' | 'docs' | 'workflows' | 'utility' | 'research'; const PACK_SKILLS: Record = { ideas: IDEA_SKILLS, + docs: DOCS_SKILLS, workflows: WORKFLOW_SKILLS, utility: UTILITY_SKILLS, research: RESEARCH_SKILLS, @@ -56,6 +58,7 @@ function createDependencies(): RemoveSkillsDependencies { function isPackName(value: string): value is PackName { return ( value === 'ideas' || + value === 'docs' || value === 'workflows' || value === 'utility' || value === 'research' @@ -74,7 +77,7 @@ export function createRemoveSkillsCommand( .description('Remove installed skills by pack') .requiredOption( '--pack ', - 'Skill pack to remove (ideas|workflows|utility|research)', + 'Skill pack to remove (ideas|docs|workflows|utility|research)', ) .option('--dry-run', 'Preview removal without applying') .action(async (options: RemoveSkillsOptions, command: Command) => { @@ -86,7 +89,7 @@ export function createRemoveSkillsCommand( const rawPack = (options.pack ?? '').toLowerCase(); if (!isPackName(rawPack)) { throw new Error( - `Invalid pack: ${options.pack}. Expected one of: ideas, workflows, utility, research.`, + `Invalid pack: ${options.pack}. Expected one of: ideas, docs, workflows, utility, research.`, ); } diff --git a/packages/cli/src/commands/tools/list/list-tools.test.ts b/packages/cli/src/commands/tools/list/list-tools.test.ts index d0fcc05c9..7f8969a0d 100644 --- a/packages/cli/src/commands/tools/list/list-tools.test.ts +++ b/packages/cli/src/commands/tools/list/list-tools.test.ts @@ -54,7 +54,7 @@ describe('runListTools', () => { ...sampleTool, name: 'oat-docs-analyze', scope: 'user', - pack: 'utility', + pack: 'docs', }, ], }); diff --git a/packages/cli/src/commands/tools/remove/index.ts b/packages/cli/src/commands/tools/remove/index.ts index 3ef8cc466..d2218eb9c 100644 --- a/packages/cli/src/commands/tools/remove/index.ts +++ b/packages/cli/src/commands/tools/remove/index.ts @@ -56,6 +56,7 @@ const defaultSyncDependencies: AutoSyncDependencies = { const VALID_PACKS = [ 'core', 'ideas', + 'docs', 'workflows', 'utility', 'project-management', @@ -71,7 +72,7 @@ export function createToolsRemoveCommand( .argument('[name]', 'Tool name to remove') .option( '--pack ', - 'Remove all tools in a pack (core|ideas|workflows|utility|project-management|research)', + 'Remove all tools in a pack (core|ideas|docs|workflows|utility|project-management|research)', ) .option('--all', 'Remove all installed tools') .option('--dry-run', 'Preview removals without applying') diff --git a/packages/cli/src/commands/tools/remove/remove-tools.test.ts b/packages/cli/src/commands/tools/remove/remove-tools.test.ts index 04f4880ff..582538550 100644 --- a/packages/cli/src/commands/tools/remove/remove-tools.test.ts +++ b/packages/cli/src/commands/tools/remove/remove-tools.test.ts @@ -120,7 +120,7 @@ describe('removeTools', () => { type: 'agent', pack: 'workflows', }), - createTool({ name: 'oat-docs-analyze', pack: 'utility' }), + createTool({ name: 'oat-docs-analyze', pack: 'docs' }), ]; const deps = createDeps({ project: tools }); diff --git a/packages/cli/src/commands/tools/shared/scan-tools.test.ts b/packages/cli/src/commands/tools/shared/scan-tools.test.ts index c668eacd4..53c157290 100644 --- a/packages/cli/src/commands/tools/shared/scan-tools.test.ts +++ b/packages/cli/src/commands/tools/shared/scan-tools.test.ts @@ -244,7 +244,7 @@ describe('scanTools', () => { expect(result[0]!.pack).toBe('workflows'); }); - it('detects utility skills pack membership', async () => { + it('detects docs skills pack membership', async () => { const deps = createMockDeps({ readdir: async (path: string) => { if (path.includes('.agents/skills')) return ['oat-docs-analyze']; @@ -264,6 +264,29 @@ describe('scanTools', () => { dependencies: deps, }); + expect(result[0]!.pack).toBe('docs'); + }); + + it('detects utility skills pack membership', async () => { + const deps = createMockDeps({ + readdir: async (path: string) => { + if (path.includes('.agents/skills')) return ['create-agnostic-skill']; + return []; + }, + dirExists: async (path: string) => { + if (path.includes('assets/skills/create-agnostic-skill')) return true; + return false; + }, + getSkillVersion: async () => '1.0.0', + }); + + const result = await scanTools({ + scope: 'user', + scopeRoot: '/home/user', + assetsRoot: '/assets', + dependencies: deps, + }); + expect(result[0]!.pack).toBe('utility'); }); diff --git a/packages/cli/src/commands/tools/shared/scan-tools.ts b/packages/cli/src/commands/tools/shared/scan-tools.ts index 563a4f81b..552b6da2d 100644 --- a/packages/cli/src/commands/tools/shared/scan-tools.ts +++ b/packages/cli/src/commands/tools/shared/scan-tools.ts @@ -2,6 +2,7 @@ import { readdir } from 'node:fs/promises'; import { join } from 'node:path'; import { CORE_SKILLS } from '@commands/init/tools/core/install-core'; +import { DOCS_SKILLS } from '@commands/init/tools/docs/install-docs'; import { IDEA_SKILLS } from '@commands/init/tools/ideas/install-ideas'; import { RESEARCH_AGENTS, @@ -59,6 +60,7 @@ const defaultDependencies: ScanToolsDependencies = { function resolveSkillPack(name: string): PackName | 'custom' { if ((CORE_SKILLS as readonly string[]).includes(name)) return 'core'; if ((IDEA_SKILLS as readonly string[]).includes(name)) return 'ideas'; + if ((DOCS_SKILLS as readonly string[]).includes(name)) return 'docs'; if ((WORKFLOW_SKILLS as readonly string[]).includes(name)) return 'workflows'; if ((UTILITY_SKILLS as readonly string[]).includes(name)) return 'utility'; if ((PROJECT_MANAGEMENT_SKILLS as readonly string[]).includes(name)) diff --git a/packages/cli/src/commands/tools/shared/types.ts b/packages/cli/src/commands/tools/shared/types.ts index e814df12b..b70fb96dd 100644 --- a/packages/cli/src/commands/tools/shared/types.ts +++ b/packages/cli/src/commands/tools/shared/types.ts @@ -3,6 +3,7 @@ import type { ConcreteScope } from '@shared/types'; export type PackName = | 'core' | 'ideas' + | 'docs' | 'workflows' | 'utility' | 'project-management' diff --git a/packages/cli/src/commands/tools/update/index.ts b/packages/cli/src/commands/tools/update/index.ts index e4c853a17..711f3084a 100644 --- a/packages/cli/src/commands/tools/update/index.ts +++ b/packages/cli/src/commands/tools/update/index.ts @@ -56,6 +56,7 @@ const defaultSyncDependencies: AutoSyncDependencies = { const VALID_PACKS = [ 'core', 'ideas', + 'docs', 'workflows', 'utility', 'project-management', @@ -71,7 +72,7 @@ export function createToolsUpdateCommand( .argument('[name]', 'Tool name to update') .option( '--pack ', - 'Update all tools in a pack (core|ideas|workflows|utility|project-management|research)', + 'Update all tools in a pack (core|ideas|docs|workflows|utility|project-management|research)', ) .option('--all', 'Update all outdated tools') .option('--dry-run', 'Preview updates without applying') diff --git a/packages/cli/src/commands/tools/update/update-tools.test.ts b/packages/cli/src/commands/tools/update/update-tools.test.ts index cde57d45f..a394516ca 100644 --- a/packages/cli/src/commands/tools/update/update-tools.test.ts +++ b/packages/cli/src/commands/tools/update/update-tools.test.ts @@ -175,7 +175,7 @@ describe('updateTools', () => { createTool({ name: 'oat-project-new', pack: 'workflows' }), createTool({ name: 'oat-docs-analyze', - pack: 'utility', + pack: 'docs', status: 'current', }), ];