diff --git a/.context/decisions/016-multi-agent-harness.md b/.context/decisions/016-multi-agent-harness.md new file mode 100644 index 0000000..c671cb3 --- /dev/null +++ b/.context/decisions/016-multi-agent-harness.md @@ -0,0 +1,111 @@ +# ADR-016: Multi-Agent Harness via Adapter Registry + +**Status:** Accepted +**Date:** 2026-06-02 +**Version:** 1.0 +**Deciders:** Nicholas (Gocase) + +## Context + +dotcontext was built Claude-Code-first (ADR-004): a project-root `CLAUDE.md` plus `.claude/` +commands, agents, and skills. ADR-004 explicitly accepted "tightly coupled to Claude Code +(won't work with other AI tools)" as a tradeoff. + +The toolkit's value — the curated project instructions, decisions, and skills — is agent-agnostic. +Coding agents have converged on a near-common convention: the **AGENTS.md** open standard +(Linux-Foundation-stewarded, plain Markdown, nearest-file-wins). GitHub's spec-kit ships per-agent +integrations; we want the same reach without abandoning the Claude-first ergonomics. + +Research (2026) on where each agent reads its project instruction/memory file: + +| Agent | Instructions file | Reads `AGENTS.md` natively? | +|-------|-------------------|-----------------------------| +| Claude Code | `CLAUDE.md` | No (but supports `@import`) | +| OpenAI Codex | `AGENTS.md` | Yes (originator) | +| opencode | `AGENTS.md` (also reads `CLAUDE.md`) | Yes | +| GitHub Copilot | `AGENTS.md` / `.github/copilot-instructions.md` | Yes (since Aug 2025) | +| Cursor (IDE + `cursor-agent` CLI) | `AGENTS.md` / `.cursor/rules/*.mdc` | Yes | +| Gemini CLI | `GEMINI.md` | No (but supports `@import`; configurable) | + +## Decision + +### 1. Extensible adapter registry + +A registry (`src/setup/agents.sh`) declares each supported agent as a small record: `id`, +display name, detection command (`command -v`), instructions filename, and emit mode. Adding an +agent is one entry — nothing else in the toolkit hard-codes an agent. Initial set: +`claude, codex, opencode, gemini, copilot, cursor` (the Cursor entry covers both the IDE and the +`cursor-agent` CLI). + +### 2. `AGENTS.md` is the single canonical source of project instructions + +The real instruction content lives in **`AGENTS.md` at the repo root**. This natively covers +**codex, opencode, copilot, and cursor** with zero extra files. The two agents that don't read +`AGENTS.md` get thin **import stubs** pointing at it (both support `@import`): + +- `CLAUDE.md` → `@AGENTS.md` (for Claude Code) +- `GEMINI.md` → `@AGENTS.md` (for Gemini CLI) + +Single source, no duplication, no drift; each editor edits `AGENTS.md`. + +### 3. Interactive agent detection (no new CLI flags) + +`dotcontext init` detects which agent CLIs are installed and emits the matching instruction files; +in interactive mode it confirms the selection. `--yes` sets up all detected agents (falling back +to Claude when none are detected). This honors the minimal-CLI constraint (ADR-007) — no +`--agent` flag, the flow lives inside `init`. + +### 4. Safe migration for existing projects + +`dotcontext update` migrates legacy single-file projects: when a content-bearing `CLAUDE.md` +exists and `AGENTS.md` does not, it offers to make `AGENTS.md` canonical (move the content) and +leave `CLAUDE.md` as an `@AGENTS.md` stub. This is content-preserving and behavior-preserving for +Claude (it still reads the same content via the import). + +### 5. Scope of this decision (phase 2a) + +This ADR covers the **instructions file** only. Skills and hooks portability is addressed in +**ADR-017**; porting slash commands to each agent's native format is deferred to a later phase. + +> **Correction (2026-06-03):** an earlier draft of this ADR claimed "Codex/Gemini/etc. have no +> structured-question API," used to justify a degraded `{{ASK}}` fallback. That premise is **false**. +> Current research (see ADR-017) confirms **all six agents ship a model-callable structured-question +> tool** (Claude `AskUserQuestion`, Codex `request_user_input`, opencode `question`, Gemini +> `ask_user`, Copilot `ask_user`, Cursor `cursor/ask_question`) **and** lifecycle hooks. So command +> portability (ADR-018) renders `{{ASK}}` to each agent's **native** question tool — it +> does not degrade to free text. The `--version --json` handshake (ADR-015) flips `multiagent: true` +> and lists supported `agents` as adapters land. + +## Consequences + +### Positive +- The toolkit works across Claude, Codex, opencode, Copilot, Cursor, and Gemini from one source. +- Adding a new agent is a single registry entry. +- Existing Claude-only projects keep working and migrate safely. + +### Negative +- A root `AGENTS.md` plus possible `CLAUDE.md`/`GEMINI.md` stubs is slightly more surface than a + lone `CLAUDE.md`. +- Import resolution depends on each agent honoring `@import`; agents that don't are covered by + reading `AGENTS.md` directly, but a future agent that does neither would need a real copy. +- Command/skill portability is **not** solved here (phase 2b). + +## Alternatives Considered + +1. **Keep `CLAUDE.md` canonical, point `AGENTS.md` at it via `@import`** — rejected: Codex/Copilot/ + Cursor don't resolve `@import`, so they'd see a literal pointer instead of content. +2. **Symlinks (`AGENTS.md` → `CLAUDE.md`)** — rejected: fragile on Windows/WSL (ADR cross-platform rule) and in git. +3. **Full content copies per agent** — rejected: guarantees drift across files. +4. **Per-agent zip artifacts (old spec-kit model)** — rejected: doesn't fit the single-bash-executable + curl distribution (ADR-001/002). + +## History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | 2026-06-02 | Initial decision (phase 2a: instructions file across agents) | + +## Related +- ADR-004: Claude Code integration (this extends it beyond Claude) +- ADR-005: Mandatory AskUserQuestion (will get a v2.0 for command portability in phase 2b) +- ADR-007: CLI simplification (interactive detection, no new flags) +- ADR-015: Capability discovery & update awareness (the handshake this populates) diff --git a/.context/decisions/017-skills-and-hooks-portability.md b/.context/decisions/017-skills-and-hooks-portability.md new file mode 100644 index 0000000..b4531d8 --- /dev/null +++ b/.context/decisions/017-skills-and-hooks-portability.md @@ -0,0 +1,102 @@ +# ADR-017: Harness Selection, Skills & Hooks Portability + +**Status:** Accepted +**Date:** 2026-06-03 +**Version:** 1.0 +**Deciders:** Nicholas (Gocase) + +## Context + +ADR-016 made the project-instructions file (`AGENTS.md`) multi-agent. Two follow-ups remained: +**skills** and **hooks**. Research (June 2026) overturned an earlier assumption: **all six target +agents now implement the open Agent Skills (`SKILL.md`) standard, ship lifecycle hooks, and expose a +model-callable structured-question tool** (so the `AskUserQuestion` premise in ADR-016's first draft +was wrong — corrected there). + +Skills directory coverage: + +| Reads `.agents/skills/` | Reads `.claude/skills/` | +|-------------------------|--------------------------| +| Codex, opencode, Gemini, Copilot, Cursor | Claude, opencode, Copilot, Cursor | + +No single directory covers all six: **Claude only reads `.claude/skills/`**, and **Codex/Gemini do +not read `.claude/skills/`**. + +A separate problem surfaced: the old `init` emitted **all** of `.claude/` (commands, agents, +statusline, settings) plus `CLAUDE.md` unconditionally — junk for a user who only runs Codex or +Gemini. + +## Decision + +### 1. Harness selection — only emit what the chosen agents use + +`dotcontext init` resolves a set of harnesses and emits files **only** for them: +- **Interactive** (default): confirm each detected agent (default = detected set). +- **`--agents claude,codex`** flag: non-interactive/scripted selection (a deliberate, scoped + exception to the minimal-CLI rule of ADR-007, for spec-kit `--integration` parity). +- **`--yes`**: all detected agents (fallback to `claude` if none). + +Emission gating: +- `AGENTS.md` (canonical instructions) + `.context/` skeleton + MCP → **always** (harness-agnostic). +- `CLAUDE.md` stub + **all of `.claude/`** (commands, agents, statusline, `.claudeignore`) → **only if + Claude is selected**. +- `GEMINI.md` stub → only if Gemini selected. +- A Codex-only project therefore gets just `AGENTS.md` + `.agents/skills/` + a Codex hook — no `.claude/`. + +### 2. Skills — `.agents/skills/` canonical, mirrored to `.claude/skills/` + +The shared `SKILL.md` files are emitted to the physical directory matching the selection: +- Claude selected → physical `.claude/skills/`; if an `AGENTS.md`-reading agent is also selected, + `.agents/skills/` is a symlink to it (copy fallback for filesystems without symlinks). +- Claude not selected → physical `.agents/skills/`. + +dotcontext owns these files, so a single physical copy + symlink avoids drift (no `@import` exists for +skills, unlike instructions). + +### 3. Hooks — a notification hook per selected harness + +Each selected harness gets a "task finished / needs attention" notification wired in its native config: +- Claude → `.claude/settings.json` (existing: notify + tool-failure-guard). +- Codex → `.codex/hooks.json`; Gemini → `.gemini/settings.json`; Copilot → `.github/hooks/dotcontext-notify.json`; + Cursor → `.cursor/hooks.json`; opencode → `.opencode/plugins/dotcontext-notify.js` (opencode has no + declarative hooks — only JS/TS plugins). +- All point at a shared, **arg-driven** `notify.sh` (it takes title/message/sound as args and ignores + stdin, so the same script works regardless of each agent's hook JSON contract). Create-only — never + clobbers an existing agent config. + +**Scope limits (intentional):** only the **notification** hook ports. The richer +`tool-failure-guard` stays **Claude-only** (it depends on Claude's `PostToolUseFailure` semantics, which +don't map uniformly). Non-Claude hook configs are validated as well-formed (JSON/TOML/JS) but are +**best-effort** — they are not end-to-end tested against each agent runtime. + +## Consequences + +### Positive +- No junk: a project only carries files for the harnesses it actually uses. +- Skills and notification hooks work across all six agents from shared sources. +- Confirms command portability (ADR-018) is viable — every agent has a native + structured-question tool. + +### Negative +- Per-agent hook configs are best-effort (not runtime-tested per agent); event-name/semantics differ + (e.g. Gemini uses `AfterAgent`, not `Stop`). +- `tool-failure-guard` remains Claude-only. +- A `--agents` flag widens the CLI surface (scoped exception, justified above). + +## Alternatives Considered + +1. **Emit everything for all agents always** — rejected: dumps junk (the problem this fixes). +2. **One skills dir for all** — impossible: Claude and Codex/Gemini read disjoint directories. +3. **Duplicate skill copies per ecosystem** — rejected: drift; symlink keeps one source. +4. **Port `tool-failure-guard` everywhere** — rejected for now: no uniform failure-event semantics. + +## History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 | 2026-06-03 | Initial decision (harness selection, skills, notification hooks) | + +## Related +- ADR-016: Multi-agent harness (instructions file) — this builds on it +- ADR-005: Mandatory AskUserQuestion — ADR-018 renders `{{ASK}}` to each agent's native question tool to satisfy it cross-agent +- ADR-007: CLI simplification — `--agents` is a scoped exception diff --git a/.context/decisions/018-command-portability-and-invocation-modes.md b/.context/decisions/018-command-portability-and-invocation-modes.md new file mode 100644 index 0000000..a03ea16 --- /dev/null +++ b/.context/decisions/018-command-portability-and-invocation-modes.md @@ -0,0 +1,79 @@ +# ADR-018: Command Portability & Invocation Modes (phase 2b plan) + +**Status:** Proposed +**Date:** 2026-06-03 +**Deciders:** Nicholas (Gocase) + +## Context + +ADR-016/017 made instructions, skills, and hooks multi-agent. The remaining piece (phase 2b) is +porting the 12 slash commands. Two facts shape the design: + +1. **Commands and skills have converged** (Claude Code): a `/x` command is effectively a skill with + `disable-model-invocation: true`. Skills (`SKILL.md`) port cleanly to all six agents (ADR-017); + classic per-repo commands do not (Codex prompts are global, Cursor has no file-based slash command). +2. **Every agent ships a structured-question tool** (ADR-016 correction): Claude `AskUserQuestion`, + Codex `request_user_input`, opencode `question`, Gemini `ask_user`, Copilot `ask_user`, Cursor + `cursor/ask_question`. So requirement-gathering (ADR-005) is satisfiable on every agent. + +The tempting shortcut — "make everything a skill" — is **unsafe**: skills are model-auto-invocable, +and `disable-model-invocation` (explicit-only) is honored only by Claude and Cursor. Auto-firing a +side-effecting command (`/commit`, `/create-pr`) on the other agents would be a foot-gun. + +## Decision + +### 1. Each artifact declares an invocation mode + +- **`skill`** — auto-discoverable (model invokes by `description`) **and** explicitly invocable. For + read-mostly knowledge/analysis with no dangerous side effects. +- **`command`** — explicit-only. For deliberate, side-effecting, or outward-facing actions. + +Classification (skills for knowledge, commands for action): + +| Mode `skill` | Mode `command` (explicit-only) | +|---|---| +| `bug-reproduction`, `batch-operations`, `git-platform` (existing guides) | `setup-context`, `generate-prp`, `execute-prp`, `commit`, `create-pr`, `pr-comment`, `add-decision`, `add-skill`, `add-command` | +| **`code-review`**, **`deep-context`** (read-mostly analysis — promoted from command) | `fix-bug` (makes changes; leans on the `bug-reproduction` skill) | + +Rationale for the promotions: `code-review` and `deep-context` are read-mostly, and "review my changes" +/ "help me understand X" are natural auto-triggers — discovery adds value with no side-effect risk, and +the explicit `/name` still works. + +### 2. Emit per agent by mode + +- `skill` mode → `SKILL.md` (already ports to all six — ADR-017). +- `command` mode → each agent's **explicit** primitive: Claude command, opencode command, Copilot + prompt file, Gemini custom command (TOML), Cursor skill + `disable-model-invocation`, Codex prompt + (global `~/.codex/prompts/`, namespaced). Where an agent only offers auto-skills, use its explicit + flag if honored, otherwise fall back to documenting the workflow in `AGENTS.md`. + +### 3. Portable `{{ASK}}` directive + +Command/skill bodies express requirement-gathering with a portable `{{ASK: question | optA | optB }}` +directive. At emit time it renders to each agent's **native** structured-question tool — it does **not** +degrade to free text. This satisfies ADR-005's clarity-assessed questioning on every agent. + +## Consequences + +- **Positive:** preserves the safe boundary (knowledge = skill, action = command); no side-effecting + auto-invocation; `{{ASK}}` keeps the ADR-005 contract everywhere. +- **Negative:** the emitter must implement per-agent command formats + the `{{ASK}}` renderer; Codex + commands remain global; Cursor commands ride on the skill+flag path. + +## Alternatives Considered + +1. **All artifacts as auto-skills** — rejected: unsafe (auto-firing side effects; `disable-model-invocation` + not universal). +2. **All as classic commands** — rejected: don't port (Codex global, Cursor none). +3. **Free-text fallback for `{{ASK}}`** — rejected: unnecessary, every agent has a native question tool. + +## History + +| Version | Date | Changes | +|---------|------|---------| +| 1.0 (proposed) | 2026-06-03 | Initial plan for phase 2b | + +## Related +- ADR-005: Mandatory AskUserQuestion (clarity assessment) — `{{ASK}}` is how it's satisfied cross-agent +- ADR-016: Multi-agent harness (instructions) +- ADR-017: Harness selection, skills & hooks portability diff --git a/CHANGELOG.md b/CHANGELOG.md index c5cf9ea..dc5feda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.16.0](https://github.com/goca-se/dotcontext/compare/v0.15.0...v0.16.0) (2026-06-03) + +### Features + +* **multi-agent support** — dotcontext now targets six harnesses (Claude Code, OpenAI Codex, opencode, Gemini CLI, GitHub Copilot, Cursor incl. `cursor-agent`) instead of Claude only (ADR-016, ADR-017): + * **extensible adapter registry** (`src/setup/agents.sh`) — each agent is one entry (`id`, name, detection, instructions file, emit mode). Adding an agent is a single case arm + * **harness selection — only emit what you choose** — `init` confirms each detected agent interactively, or takes `--agents claude,codex` (non-interactive), or `--yes` (all detected). A Codex-only project gets **no `.claude/`** — no junk + * **instructions** — canonical **`AGENTS.md`** read natively by Codex/opencode/Copilot/Cursor; **Claude** (`CLAUDE.md`) and **Gemini** (`GEMINI.md`) via thin `@AGENTS.md` import stubs. Single source, no duplication + * **skills** — shared `SKILL.md` content emitted to `.agents/skills/` (Codex/opencode/Gemini/Copilot/Cursor) mirrored to `.claude/skills/` (Claude) via symlink (copy fallback) + * **hooks** — a "task finished / needs attention" notification wired per selected harness in its native config (`.codex/hooks.json`, `.gemini/settings.json`, `.github/hooks/`, `.cursor/hooks.json`, opencode JS plugin); Claude also keeps the tool-failure guard + * **`update` migrates legacy projects** — a content-bearing `CLAUDE.md` with no `AGENTS.md` is offered migration into the shared `AGENTS.md` (content- and behavior-preserving), plus a `GEMINI.md` stub when the Gemini CLI is present + * **`--version --json`** reports `multiagent: true`, `skills: true`, `hooks: true` and the supported `agents` list; **`doctor`** is `AGENTS.md`-aware and reports detected agents + +### Notes + +* Slash **commands** remain Claude-native; per-agent command ports (ADR-018 — invocation-mode classification + a portable `{{ASK}}` rendered to each agent's native structured-question tool) are the next phase. Non-Claude hook configs are validated as well-formed but are best-effort (not runtime-tested per agent); the tool-failure guard stays Claude-only. + ## [0.15.0](https://github.com/goca-se/dotcontext/compare/v0.14.2...v0.15.0) (2026-06-02) ### Features diff --git a/Makefile b/Makefile index d4ae760..7738f25 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ SOURCES = \ src/core/utils.sh \ src/setup/notifications.sh \ src/setup/mcp.sh \ + src/setup/agents.sh \ src/commands/init.sh \ src/commands/update.sh \ src/commands/doctor.sh \ diff --git a/README.md b/README.md index 64b58d3..d8ceda3 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ This ensures your architectural decisions stay synchronized with your code. ``` your-project/ -├── CLAUDE.md # Quick reference + decision compliance rules +├── AGENTS.md # Canonical project instructions (read by Codex, opencode, Copilot, Cursor…) +├── CLAUDE.md # Thin @AGENTS.md import for Claude Code (always added for compatibility) +├── GEMINI.md # Thin @AGENTS.md import for Gemini CLI (added when Gemini is detected) ├── .context/ │ ├── CONTEXT.md # Domain knowledge │ ├── decisions/ # ADRs (versioned) @@ -159,6 +161,8 @@ your-project/ └── statusline.sh # StatusLine: model, dir, git, ctx-usage bar, cost/time/lines ``` +> Files are emitted per chosen harness (see [Multi-Agent Support](#multi-agent-support)): `AGENTS.md` and `.context/` are always written; the full `.claude/` tree and `CLAUDE.md` appear only when **Claude** is selected, `GEMINI.md` only for **Gemini**, and `.agents/skills/` only when a non-Claude agent is selected. + Additionally, `dotcontext init` configures: - **MCP servers** in `.mcp.json` (optional, prompted during init): @@ -174,6 +178,27 @@ Additionally, `dotcontext init` configures: └── settings.json # Hooks for Notification and Stop events ``` +## Multi-Agent Support + +dotcontext targets six harnesses — **Claude Code, OpenAI Codex, opencode, Gemini CLI, GitHub Copilot, and Cursor** (IDE + `cursor-agent`). `init` only writes files for the harnesses you choose, so you never get junk (no `.claude/` in a Codex-only project): + +```bash +dotcontext init # interactive: confirm each detected agent +dotcontext init --agents codex,cursor # explicit, non-interactive +dotcontext init --yes # all detected agents +``` + +What's shared vs per-harness (ADR-016, ADR-017): + +| Layer | Coverage | +| --- | --- | +| **Instructions** — canonical `AGENTS.md` | Codex/opencode/Copilot/Cursor read it natively; Claude (`CLAUDE.md`) and Gemini (`GEMINI.md`) via `@AGENTS.md` import | +| **Skills** — shared `SKILL.md` | `.agents/skills/` (Codex/opencode/Gemini/Copilot/Cursor) mirrored to `.claude/skills/` (Claude) | +| **Hooks** — notification on finish | native config per harness (`.codex/`, `.gemini/`, `.github/hooks/`, `.cursor/`, opencode plugin); Claude also gets the tool-failure guard | +| **Commands** (`.claude/commands/`) | Claude-only for now — per-agent ports are planned | + +`dotcontext update` migrates an existing single-file `CLAUDE.md` into the shared `AGENTS.md` (Claude keeps working via the import). Check `dotcontext --version --json` for the live capability/agent list. + ## Commands Reference ### CLI Commands diff --git a/dotcontext b/dotcontext index 9a0fb1d..6485b43 100755 --- a/dotcontext +++ b/dotcontext @@ -1,7 +1,7 @@ #!/bin/bash set -e -VERSION="0.15.0" +VERSION="0.16.0" REPO="goca-se/dotcontext" BRANCH="main" BASE_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}" @@ -385,18 +385,285 @@ EOF print_gray "MCP servers configured in .mcp.json" fi } +# ── Agent Adapter Registry ────────────────────────────────────────────────── +# Each supported coding agent is one entry. Adding an agent means adding it to +# AGENT_IDS and the case arms below — nothing else in the toolkit hard-codes an +# agent. (ADR-016) +# +# Emit modes for the project-instructions file: +# import — a thin stub that @-imports AGENTS.md (Claude Code, Gemini CLI) +# native — reads AGENTS.md directly; needs no file of its own (Codex, +# opencode, Copilot, Cursor incl. the cursor-agent CLI) +# +# AGENTS.md itself is the canonical single source and is always emitted. + +AGENT_IDS="claude codex opencode gemini copilot cursor" + +agent_name() { + case "$1" in + claude) echo "Claude Code" ;; + codex) echo "OpenAI Codex" ;; + opencode) echo "opencode" ;; + gemini) echo "Gemini CLI" ;; + copilot) echo "GitHub Copilot" ;; + cursor) echo "Cursor (cursor-agent)" ;; + *) echo "$1" ;; + esac +} + +# Detection: is the agent's CLI installed? +agent_detect() { + case "$1" in + claude) command -v claude >/dev/null 2>&1 ;; + codex) command -v codex >/dev/null 2>&1 ;; + opencode) command -v opencode >/dev/null 2>&1 ;; + gemini) command -v gemini >/dev/null 2>&1 ;; + copilot) command -v copilot >/dev/null 2>&1 || \ + { command -v gh >/dev/null 2>&1 && gh extension list 2>/dev/null | grep -qi copilot; } ;; + cursor) command -v cursor-agent >/dev/null 2>&1 ;; + *) return 1 ;; + esac +} + +# The instruction file this agent reads. +agent_instructions_file() { + case "$1" in + claude) echo "CLAUDE.md" ;; + gemini) echo "GEMINI.md" ;; + *) echo "AGENTS.md" ;; + esac +} + +# How this agent gets its instructions: import | native +agent_emit_mode() { + case "$1" in + claude|gemini) echo "import" ;; + *) echo "native" ;; + esac +} + +# Echo the ids of detected (installed) agents, space-separated. +detect_agents() { + local id out="" + for id in $AGENT_IDS; do + if agent_detect "$id"; then out="$out $id"; fi + done + echo "${out# }" +} + +# Echo "url|target" instruction-file seed mappings for the selected agent ids. +# AGENTS.md (canonical) is always first; import-mode agents add their stub. +# Native agents need no extra file (they read AGENTS.md directly). +# The "|" separator can't appear in a URL or a filename, so consumers parse it +# unambiguously: url="${mapping%|*}", target="${mapping##*|}". +agent_instruction_seeds() { + local selected="$1" id target seen=" " + echo "${BASE_URL}/templates/AGENTS.md|AGENTS.md" + for id in $selected; do + [ "$(agent_emit_mode "$id")" = "import" ] || continue + target="$(agent_instructions_file "$id")" + case "$seen" in *" $target "*) continue ;; esac + seen="$seen$target " + echo "${BASE_URL}/templates/${target}|${target}" + done +} + +# True if the id list contains a given agent. +agents_include() { + case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac +} + +# Resolve which harnesses to instantiate. Sets the global SELECTED_AGENTS. +# Priority: explicit --agents list > non-interactive (detected, fallback claude) +# > interactive (confirm each detected agent; offer Claude if none). +# Must NOT run inside $(...) — it prompts on stdout. (ADR-016) +SELECTED_AGENTS="" +resolve_selected_agents() { + local agents_flag="$1" skip_prompts="$2" + SELECTED_AGENTS="" + + if [ -n "$agents_flag" ]; then + local IFS=',' id + for id in $agents_flag; do + id="$(echo "$id" | tr -d '[:space:]')" + [ -z "$id" ] && continue + if agents_include "$AGENT_IDS" "$id"; then + SELECTED_AGENTS="$SELECTED_AGENTS $id" + else + print_yellow " unknown agent '$id' (skipped) — known: $AGENT_IDS" + fi + done + SELECTED_AGENTS="${SELECTED_AGENTS# }" + return 0 + fi + + local detected; detected="$(detect_agents)" + + if [ "$skip_prompts" = "true" ]; then + SELECTED_AGENTS="$detected" + [ -z "$SELECTED_AGENTS" ] && SELECTED_AGENTS="claude" + return 0 + fi + + local id + if [ -n "$detected" ]; then + print_blue "Detected agents — choose which to set up:" + for id in $detected; do + if confirm_yes " Set up for $(agent_name "$id")?"; then + SELECTED_AGENTS="$SELECTED_AGENTS $id" + fi + done + fi + SELECTED_AGENTS="${SELECTED_AGENTS# }" + + if [ -z "$SELECTED_AGENTS" ]; then + if confirm_yes " No agent selected — set up for Claude Code?"; then + SELECTED_AGENTS="claude" + fi + fi +} + +# Create $2 as a symlink to $1 (relative), or copy if symlinks aren't available. +# No-op if $2 already exists. Used to mirror the skills dir across ecosystems. +link_or_copy_dir() { + local src="$1" dst="$2" + [ -d "$src" ] || return 0 + if [ -e "$dst" ] || [ -L "$dst" ]; then return 0; fi + mkdir -p "$(dirname "$dst")" + ln -s "../$src" "$dst" 2>/dev/null || cp -r "$src" "$dst" +} + +# ── Per-agent hooks ─────────────────────────────────────────────────────────── +# Wire a "task finished / needs attention" notification per selected agent. +# Claude keeps its richer hooks (notify + tool-failure-guard) via +# setup_notifications. Non-Claude agents get a notification hook in their native +# config format, pointing at a shared, arg-driven notify.sh (it ignores stdin, +# so the same script works regardless of each agent's hook JSON contract). +# Best-effort + create-only: never clobbers an existing agent config. +NOTIFY_SHARED=".context/scripts/notify.sh" + +ensure_shared_notify() { + [ -f "$NOTIFY_SHARED" ] && return 0 + mkdir -p ".context/scripts" + download "${BASE_URL}/scripts/notify.sh" "$NOTIFY_SHARED" && chmod +x "$NOTIFY_SHARED" +} + +setup_agent_hooks() { + local selected="$1" id done_any=false + for id in $selected; do + case "$id" in + claude) + setup_notifications 2>/dev/null || true + done_any=true + ;; + codex) + ensure_shared_notify + if [ ! -f ".codex/hooks.json" ]; then + mkdir -p ".codex" + cat > ".codex/hooks.json" < ".gemini/settings.json" < ".github/hooks/dotcontext-notify.json" < ".cursor/hooks.json" < ".opencode/plugins/dotcontext-notify.js" <<'JS' +// dotcontext: fire an OS notification when the session goes idle. +export const DotcontextNotify = async ({ $ }) => ({ + event: async ({ event }) => { + if (event?.type === "session.idle") { + await $`.context/scripts/notify.sh 'opencode' 'Task completed' success`.catch(() => {}) + } + }, +}) +JS + print_gray " hooks: .opencode/plugins/dotcontext-notify.js" + done_any=true + fi + ;; + esac + done + [ "$done_any" = true ] || true +} # ── Command: init ───────────────────────────────────────────────────────────── cmd_init() { local project_name="" local skip_prompts=false local skip_setup=false + local agents_flag="" while [[ $# -gt 0 ]]; do case "$1" in -n|--name) project_name="$2"; shift 2 ;; -y|--yes) skip_prompts=true; shift ;; --no-setup) skip_setup=true; shift ;; + --agents) agents_flag="$2"; shift 2 ;; *) shift ;; esac done @@ -420,134 +687,123 @@ cmd_init() { fi fi - print_gray "Creating structure..." - - # Create directories (mkdir -p is always safe) - mkdir -p ".context/decisions" - mkdir -p ".claude/skills" - mkdir -p ".claude/skills/bug-reproduction" - mkdir -p ".claude/skills/batch-operations" - mkdir -p ".claude/skills/git-platform" - mkdir -p ".context/prp/templates" - mkdir -p ".context/prp/generated" - mkdir -p ".context/discoveries" - mkdir -p ".context/bugs" - mkdir -p ".claude/commands" - mkdir -p ".claude/scripts" - mkdir -p ".claude/agents/code-review" - mkdir -p ".claude/agents/deep-context" - mkdir -p ".claude/agents/fix-bug" - - # Download templates + # Resolve which harnesses to instantiate (sets SELECTED_AGENTS). Only the + # chosen harnesses get files — no .claude/ for a Codex-only user, etc. (ADR-016) + resolve_selected_agents "$agents_flag" "$skip_prompts" + local selected="$SELECTED_AGENTS" + if [ -z "$selected" ]; then + print_red "No harness selected — nothing to set up. Re-run and pick at least one agent." + return 1 + fi + local wants_claude=false; agents_include "$selected" claude && wants_claude=true + local wants_agents_skills=false + local _id + for _id in $selected; do [ "$_id" != "claude" ] && wants_agents_skills=true; done + + print_gray "Setting up for: $selected" + + # Harness-agnostic context skeleton (always) + mkdir -p ".context/decisions" ".context/prp/templates" ".context/prp/generated" \ + ".context/discoveries" ".context/bugs" + start_spinner "Downloading templates..." - # Helper: download only if file doesn't exist (for seed/user-customizable files) + # Helper: download only if file doesn't exist (seed/user-customizable files) local skipped_files="" download_if_missing() { local url="$1" local target="$2" if [ -f "$target" ]; then - if [ "$is_reinit" = true ]; then - skipped_files="${skipped_files}${target}\n" - fi + [ "$is_reinit" = true ] && skipped_files="${skipped_files}${target}\n" return 0 fi download "$url" "$target" } - # Seed files: user-customizable content — only created if missing - download_if_missing "${BASE_URL}/templates/.claudeignore" ".claudeignore" - download_if_missing "${BASE_URL}/templates/CLAUDE.md" "CLAUDE.md" + # ── Shared context (every harness) ── download_if_missing "${BASE_URL}/templates/.context/CONTEXT.md" ".context/CONTEXT.md" download_if_missing "${BASE_URL}/templates/.context/decisions/README.md" ".context/decisions/README.md" - download_if_missing "${BASE_URL}/templates/.claude/skills/bug-reproduction/SKILL.md" ".claude/skills/bug-reproduction/SKILL.md" - download_if_missing "${BASE_URL}/templates/.claude/skills/batch-operations/SKILL.md" ".claude/skills/batch-operations/SKILL.md" - download_if_missing "${BASE_URL}/templates/.claude/skills/git-platform/SKILL.md" ".claude/skills/git-platform/SKILL.md" download_if_missing "${BASE_URL}/templates/.context/prp/templates/feature.md" ".context/prp/templates/feature.md" - # Managed files: dotcontext commands — always downloaded (safe to overwrite) - download "${BASE_URL}/templates/.claude/commands/setup-context.md" ".claude/commands/setup-context.md" - download "${BASE_URL}/templates/.claude/commands/code-review.md" ".claude/commands/code-review.md" - download "${BASE_URL}/templates/.claude/commands/generate-prp.md" ".claude/commands/generate-prp.md" - download "${BASE_URL}/templates/.claude/commands/execute-prp.md" ".claude/commands/execute-prp.md" - download "${BASE_URL}/templates/.claude/commands/add-decision.md" ".claude/commands/add-decision.md" - download "${BASE_URL}/templates/.claude/commands/add-skill.md" ".claude/commands/add-skill.md" - download "${BASE_URL}/templates/.claude/commands/add-command.md" ".claude/commands/add-command.md" - download "${BASE_URL}/templates/.claude/commands/create-pr.md" ".claude/commands/create-pr.md" - download "${BASE_URL}/templates/.claude/commands/pr-comment.md" ".claude/commands/pr-comment.md" - download "${BASE_URL}/templates/.claude/commands/deep-context.md" ".claude/commands/deep-context.md" - download "${BASE_URL}/templates/.claude/commands/fix-bug.md" ".claude/commands/fix-bug.md" - download "${BASE_URL}/templates/.claude/commands/commit.md" ".claude/commands/commit.md" - - # StatusLine script - download "${BASE_URL}/templates/.claude/scripts/statusline.sh" ".claude/scripts/statusline.sh" - chmod +x ".claude/scripts/statusline.sh" - - # Agent definition files (always downloaded — managed by dotcontext) - download "${BASE_URL}/templates/.claude/agents/code-review/compliance-checker.md" ".claude/agents/code-review/compliance-checker.md" - download "${BASE_URL}/templates/.claude/agents/code-review/bug-detector.md" ".claude/agents/code-review/bug-detector.md" - download "${BASE_URL}/templates/.claude/agents/code-review/security-analyst.md" ".claude/agents/code-review/security-analyst.md" - download "${BASE_URL}/templates/.claude/agents/deep-context/step1-overview.md" ".claude/agents/deep-context/step1-overview.md" - download "${BASE_URL}/templates/.claude/agents/deep-context/step2-subsystems.md" ".claude/agents/deep-context/step2-subsystems.md" - download "${BASE_URL}/templates/.claude/agents/deep-context/step3-drill.md" ".claude/agents/deep-context/step3-drill.md" - download "${BASE_URL}/templates/.claude/agents/deep-context/step4-dataflow.md" ".claude/agents/deep-context/step4-dataflow.md" - download "${BASE_URL}/templates/.claude/agents/fix-bug/investigator.md" ".claude/agents/fix-bug/investigator.md" - download "${BASE_URL}/templates/.claude/agents/fix-bug/fix-conservative.md" ".claude/agents/fix-bug/fix-conservative.md" - download "${BASE_URL}/templates/.claude/agents/fix-bug/fix-minimal.md" ".claude/agents/fix-bug/fix-minimal.md" - download "${BASE_URL}/templates/.claude/agents/fix-bug/fix-refactor.md" ".claude/agents/fix-bug/fix-refactor.md" - download "${BASE_URL}/templates/.claude/agents/fix-bug/reviewer.md" ".claude/agents/fix-bug/reviewer.md" + # ── Project instructions: canonical AGENTS.md + import stubs for selected agents ── + for mapping in $(agent_instruction_seeds "$selected"); do + # mappings are "url|target" ("|" can't appear in a URL or filename) + download_if_missing "${mapping%|*}" "${mapping##*|}" + done - # Declarative cleanup: remove stale files from managed-only directories - # NOTE: .claude/commands/ is excluded — users create custom commands there via /add-command - cleanup_managed_dir ".claude/agents/code-review" \ - compliance-checker.md bug-detector.md security-analyst.md - cleanup_managed_dir ".claude/agents/deep-context" \ - step1-overview.md step2-subsystems.md step3-drill.md step4-dataflow.md - cleanup_managed_dir ".claude/agents/fix-bug" \ - investigator.md fix-conservative.md fix-minimal.md fix-refactor.md reviewer.md - cleanup_managed_dir ".claude/scripts" \ - statusline.sh notify.sh tool-failure-guard.sh + # ── Skills (shared SKILL.md content; physical home depends on selection) ── + local skills_dir + if [ "$wants_claude" = true ]; then skills_dir=".claude/skills"; else skills_dir=".agents/skills"; fi + mkdir -p "$skills_dir/bug-reproduction" "$skills_dir/batch-operations" "$skills_dir/git-platform" + download_if_missing "${BASE_URL}/templates/.claude/skills/bug-reproduction/SKILL.md" "$skills_dir/bug-reproduction/SKILL.md" + download_if_missing "${BASE_URL}/templates/.claude/skills/batch-operations/SKILL.md" "$skills_dir/batch-operations/SKILL.md" + download_if_missing "${BASE_URL}/templates/.claude/skills/git-platform/SKILL.md" "$skills_dir/git-platform/SKILL.md" + # If both Claude and an AGENTS.md-reading agent are selected, mirror so both see the skills + if [ "$wants_claude" = true ] && [ "$wants_agents_skills" = true ]; then + link_or_copy_dir ".claude/skills" ".agents/skills" + fi + + # ── Claude-only toolkit (commands, agents, statusline, ignore) ── + if [ "$wants_claude" = true ]; then + mkdir -p ".claude/commands" ".claude/scripts" \ + ".claude/agents/code-review" ".claude/agents/deep-context" ".claude/agents/fix-bug" + download_if_missing "${BASE_URL}/templates/.claudeignore" ".claudeignore" + + local c + for c in setup-context code-review generate-prp execute-prp add-decision add-skill \ + add-command create-pr pr-comment deep-context fix-bug commit; do + download "${BASE_URL}/templates/.claude/commands/${c}.md" ".claude/commands/${c}.md" + done + + download "${BASE_URL}/templates/.claude/scripts/statusline.sh" ".claude/scripts/statusline.sh" + chmod +x ".claude/scripts/statusline.sh" + + local a + for a in code-review/compliance-checker code-review/bug-detector code-review/security-analyst \ + deep-context/step1-overview deep-context/step2-subsystems deep-context/step3-drill \ + deep-context/step4-dataflow fix-bug/investigator fix-bug/fix-conservative \ + fix-bug/fix-minimal fix-bug/fix-refactor fix-bug/reviewer; do + download "${BASE_URL}/templates/.claude/agents/${a}.md" ".claude/agents/${a}.md" + done + + cleanup_managed_dir ".claude/agents/code-review" \ + compliance-checker.md bug-detector.md security-analyst.md + cleanup_managed_dir ".claude/agents/deep-context" \ + step1-overview.md step2-subsystems.md step3-drill.md step4-dataflow.md + cleanup_managed_dir ".claude/agents/fix-bug" \ + investigator.md fix-conservative.md fix-minimal.md fix-refactor.md reviewer.md + cleanup_managed_dir ".claude/scripts" \ + statusline.sh notify.sh tool-failure-guard.sh + fi stop_spinner - # Show skipped files after spinner (so output doesn't collide) if [ -n "$skipped_files" ]; then printf "$skipped_files" | while read -r f; do [ -n "$f" ] && print_gray " skipped (exists): $f" done fi - touch ".context/prp/generated/.keep" - touch ".context/discoveries/.keep" - touch ".context/bugs/.keep" + touch ".context/prp/generated/.keep" ".context/discoveries/.keep" ".context/bugs/.keep" - # Setup notifications (optional, don't fail if it doesn't work) - setup_notifications 2>/dev/null || true + # ── Hooks per selected harness (notifications; Claude also gets the failure guard) ── + setup_agent_hooks "$selected" 2>/dev/null || true - # MCP server configuration (only if .mcp.json doesn't exist) + # ── MCP servers (cross-agent; Context7/Atlassian work across harnesses) ── if [ ! -f ".mcp.json" ]; then local add_context7=false local add_atlassian=false - if [ "$skip_prompts" = false ]; then echo "" print_blue "MCP Servers" - print_gray "Configure Model Context Protocol servers for Claude Code" + print_gray "Configure Model Context Protocol servers for your agents" echo "" - - if confirm_yes " Add Context7? (up-to-date library docs for LLMs)"; then - add_context7=true - fi - - if confirm_yes " Add Atlassian? (Jira + Confluence via OAuth)"; then - add_atlassian=true - fi + confirm_yes " Add Context7? (up-to-date library docs for LLMs)" && add_context7=true + confirm_yes " Add Atlassian? (Jira + Confluence via OAuth)" && add_atlassian=true else - # --yes flag: include both by default add_context7=true add_atlassian=true fi - setup_mcp "$add_context7" "$add_atlassian" else print_gray " skipped (exists): .mcp.json" @@ -558,14 +814,17 @@ cmd_init() { offer_completion_install fi - # Substitute project name (only on fresh CLAUDE.md with placeholder) - if grep -q "{{projectName}}" "CLAUDE.md" 2>/dev/null; then - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/{{projectName}}/$project_name/g" "CLAUDE.md" - else - sed -i "s/{{projectName}}/$project_name/g" "CLAUDE.md" + # Substitute project name in any instruction file that still has the placeholder + local inst_file + for inst_file in AGENTS.md CLAUDE.md GEMINI.md; do + if grep -q "{{projectName}}" "$inst_file" 2>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/{{projectName}}/$project_name/g" "$inst_file" + else + sed -i "s/{{projectName}}/$project_name/g" "$inst_file" + fi fi - fi + done echo "" if [ "$is_reinit" = true ]; then @@ -573,18 +832,23 @@ cmd_init() { else printf " ${GREEN}${ICON_SUCCESS}${NC} Context structure created\n" fi - printf " ${GRAY}CLAUDE.md .context/ .claude/commands/ (12) .claude/agents/ (12)${NC}\n" + local _names="" _i + for _i in $selected; do _names="$_names$(agent_name "$_i"), "; done + printf " ${GRAY}Harnesses: %s${NC}\n" "${_names%, }" echo "" - if [ "$skip_setup" = true ]; then - printf " ${CYAN}Next:${NC} run ${CYAN}/setup-context${NC} in Claude Code\n" - else - if command -v claude &> /dev/null; then + # /setup-context is a Claude command — only when Claude is part of the setup + if [ "$wants_claude" = true ]; then + if [ "$skip_setup" = true ]; then + printf " ${CYAN}Next:${NC} run ${CYAN}/setup-context${NC} in Claude Code\n" + elif command -v claude &> /dev/null; then printf " ${CYAN}Running /setup-context...${NC}\n\n" claude "/setup-context" else printf " ${YELLOW}Claude CLI not found.${NC} Run ${CYAN}/setup-context${NC} manually in Claude Code.\n" fi + else + printf " ${CYAN}Next:${NC} edit ${CYAN}AGENTS.md${NC} and ${CYAN}.context/CONTEXT.md${NC} to describe your project\n" fi echo "" } @@ -738,6 +1002,54 @@ cmd_update_templates() { print_green " Migrated remaining skills from .context/skills/ → .claude/skills/" fi + # Migration: adopt AGENTS.md as the canonical instruction source (ADR-016). + # Legacy projects have a content-bearing CLAUDE.md and no AGENTS.md. Moving + # the content to AGENTS.md (and leaving CLAUDE.md as an @import stub) is + # content- and behavior-preserving for Claude, and lets the other agents read + # the same instructions natively. + local _stub_comment='' + if [ -f "CLAUDE.md" ] && [ ! -f "AGENTS.md" ] && ! grep -q '@AGENTS.md' "CLAUDE.md" 2>/dev/null; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would migrate CLAUDE.md → AGENTS.md (+ CLAUDE.md import stub)" + else + local do_migrate=false + if [ "$auto_yes" = "true" ]; then + do_migrate=true + elif confirm_yes " Adopt AGENTS.md as the shared instructions file? (Claude keeps working via import)"; then + do_migrate=true + fi + if [ "$do_migrate" = true ]; then + mv "CLAUDE.md" "AGENTS.md" + printf '%s\n\n%s\n' "$_stub_comment" '@AGENTS.md' > "CLAUDE.md" + print_green " Migrated CLAUDE.md → AGENTS.md (Claude reads it via @import)" + fi + fi + fi + + # Add a Gemini import stub when the Gemini CLI is present and AGENTS.md exists. + if agent_detect gemini && [ -f "AGENTS.md" ] && [ ! -f "GEMINI.md" ]; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would add GEMINI.md (imports AGENTS.md)" + else + printf '%s\n\n%s\n' "$_stub_comment" '@AGENTS.md' > "GEMINI.md" + print_green " Added GEMINI.md (imports AGENTS.md)" + fi + fi + + # Mirror skills to .agents/skills/ when a non-Claude agent is present (ADR-017). + if [ -d ".claude/skills" ] && [ ! -e ".agents/skills" ]; then + local _na=false _aid + for _aid in $(detect_agents); do [ "$_aid" != "claude" ] && _na=true; done + if [ "$_na" = true ]; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would mirror .claude/skills → .agents/skills (non-Claude agent detected)" + else + link_or_copy_dir ".claude/skills" ".agents/skills" + print_green " Mirrored skills to .agents/skills/" + fi + fi + fi + start_spinner "Checking templates..." # MANAGED templates: dotcontext-owned code (commands, templates that drive commands). @@ -774,6 +1086,7 @@ cmd_update_templates() { # SEED templates: created once during init, customized by user or /setup-context. # Only added if missing — never offered for overwrite to protect user content. declare -a seed_templates=( + "templates/AGENTS.md:AGENTS.md" "templates/.context/decisions/README.md:.context/decisions/README.md" "templates/.claude/skills/bug-reproduction/SKILL.md:.claude/skills/bug-reproduction/SKILL.md" ) @@ -1058,17 +1371,20 @@ cmd_doctor() { check_fail ".context/ directory — missing (run dotcontext init)" fi - # Check: CLAUDE.md exists and non-empty - if [ -s "CLAUDE.md" ]; then - if grep -q "{{projectName}}" "CLAUDE.md" 2>/dev/null; then - check_warn "CLAUDE.md — exists but has unfilled placeholders" + # Check: project instructions (AGENTS.md is canonical; CLAUDE.md/GEMINI.md may import it) + local inst="" + [ -s "AGENTS.md" ] && inst="AGENTS.md" + [ -z "$inst" ] && [ -s "CLAUDE.md" ] && inst="CLAUDE.md" + if [ -n "$inst" ]; then + if grep -q "{{projectName}}" "$inst" 2>/dev/null; then + check_warn "$inst — exists but has unfilled placeholders" + elif [ "$inst" = "CLAUDE.md" ] && [ ! -f "AGENTS.md" ]; then + check_warn "CLAUDE.md present but no AGENTS.md — run 'dotcontext update' to adopt the shared instructions file" else - check_pass "CLAUDE.md populated" + check_pass "Project instructions populated ($inst)" fi - elif [ -f "CLAUDE.md" ]; then - check_warn "CLAUDE.md — exists but empty" else - check_fail "CLAUDE.md — missing" + check_fail "Project instructions — missing (run dotcontext init)" fi # Check: CONTEXT.md exists and non-empty @@ -1160,6 +1476,17 @@ cmd_doctor() { check_warn "Legacy global hooks detected in ~/.claude/settings.json — remove manually to avoid duplicates" fi + # Check: detected agents (informational — which agent CLIs are on PATH) + local detected_agents + detected_agents="$(detect_agents)" + if [ -n "$detected_agents" ]; then + local _names="" _aid + for _aid in $detected_agents; do _names="$_names$(agent_name "$_aid"), "; done + check_pass "Agents detected: ${_names%, }" + else + check_warn "No supported agent CLI detected on PATH" + fi + # Check: CLI update available (cached, best-effort — silent when offline) local latest_version latest_version=$(fetch_latest_version 2>/dev/null || echo "") @@ -1215,6 +1542,7 @@ cmd_help() { # Init Options printf " ${BLUE}${BOLD}Init Options${NC}\n" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--name, -n " "Project name" + printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--agents " "Harnesses to set up (e.g. claude,codex). Default: detected" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--yes, -y" "Skip prompts, use defaults" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--no-setup" "Skip automatic /setup-context" echo "" @@ -1279,6 +1607,15 @@ cmd_version() { esac done + # Build the supported-agents array from the adapter registry (ADR-016) + local id agents_json="" agents_list="" + for id in $AGENT_IDS; do + agents_json="${agents_json}\"$id\", " + agents_list="${agents_list}$id " + done + agents_json="[${agents_json%, }]" + agents_list="${agents_list% }" + if [ "$json" = true ]; then cat </dev/null; then - check_warn "CLAUDE.md — exists but has unfilled placeholders" + # Check: project instructions (AGENTS.md is canonical; CLAUDE.md/GEMINI.md may import it) + local inst="" + [ -s "AGENTS.md" ] && inst="AGENTS.md" + [ -z "$inst" ] && [ -s "CLAUDE.md" ] && inst="CLAUDE.md" + if [ -n "$inst" ]; then + if grep -q "{{projectName}}" "$inst" 2>/dev/null; then + check_warn "$inst — exists but has unfilled placeholders" + elif [ "$inst" = "CLAUDE.md" ] && [ ! -f "AGENTS.md" ]; then + check_warn "CLAUDE.md present but no AGENTS.md — run 'dotcontext update' to adopt the shared instructions file" else - check_pass "CLAUDE.md populated" + check_pass "Project instructions populated ($inst)" fi - elif [ -f "CLAUDE.md" ]; then - check_warn "CLAUDE.md — exists but empty" else - check_fail "CLAUDE.md — missing" + check_fail "Project instructions — missing (run dotcontext init)" fi # Check: CONTEXT.md exists and non-empty @@ -148,6 +151,17 @@ cmd_doctor() { check_warn "Legacy global hooks detected in ~/.claude/settings.json — remove manually to avoid duplicates" fi + # Check: detected agents (informational — which agent CLIs are on PATH) + local detected_agents + detected_agents="$(detect_agents)" + if [ -n "$detected_agents" ]; then + local _names="" _aid + for _aid in $detected_agents; do _names="$_names$(agent_name "$_aid"), "; done + check_pass "Agents detected: ${_names%, }" + else + check_warn "No supported agent CLI detected on PATH" + fi + # Check: CLI update available (cached, best-effort — silent when offline) local latest_version latest_version=$(fetch_latest_version 2>/dev/null || echo "") diff --git a/src/commands/help.sh b/src/commands/help.sh index 6b2eab9..ef0eef6 100644 --- a/src/commands/help.sh +++ b/src/commands/help.sh @@ -32,6 +32,7 @@ cmd_help() { # Init Options printf " ${BLUE}${BOLD}Init Options${NC}\n" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--name, -n " "Project name" + printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--agents " "Harnesses to set up (e.g. claude,codex). Default: detected" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--yes, -y" "Skip prompts, use defaults" printf " ${YELLOW}%-${opt_col}s${NC}%s\n" "--no-setup" "Skip automatic /setup-context" echo "" @@ -96,6 +97,15 @@ cmd_version() { esac done + # Build the supported-agents array from the adapter registry (ADR-016) + local id agents_json="" agents_list="" + for id in $AGENT_IDS; do + agents_json="${agents_json}\"$id\", " + agents_list="${agents_list}$id " + done + agents_json="[${agents_json%, }]" + agents_list="${agents_list% }" + if [ "$json" = true ]; then cat </dev/null || true + # ── Hooks per selected harness (notifications; Claude also gets the failure guard) ── + setup_agent_hooks "$selected" 2>/dev/null || true - # MCP server configuration (only if .mcp.json doesn't exist) + # ── MCP servers (cross-agent; Context7/Atlassian work across harnesses) ── if [ ! -f ".mcp.json" ]; then local add_context7=false local add_atlassian=false - if [ "$skip_prompts" = false ]; then echo "" print_blue "MCP Servers" - print_gray "Configure Model Context Protocol servers for Claude Code" + print_gray "Configure Model Context Protocol servers for your agents" echo "" - - if confirm_yes " Add Context7? (up-to-date library docs for LLMs)"; then - add_context7=true - fi - - if confirm_yes " Add Atlassian? (Jira + Confluence via OAuth)"; then - add_atlassian=true - fi + confirm_yes " Add Context7? (up-to-date library docs for LLMs)" && add_context7=true + confirm_yes " Add Atlassian? (Jira + Confluence via OAuth)" && add_atlassian=true else - # --yes flag: include both by default add_context7=true add_atlassian=true fi - setup_mcp "$add_context7" "$add_atlassian" else print_gray " skipped (exists): .mcp.json" @@ -171,14 +162,17 @@ cmd_init() { offer_completion_install fi - # Substitute project name (only on fresh CLAUDE.md with placeholder) - if grep -q "{{projectName}}" "CLAUDE.md" 2>/dev/null; then - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s/{{projectName}}/$project_name/g" "CLAUDE.md" - else - sed -i "s/{{projectName}}/$project_name/g" "CLAUDE.md" + # Substitute project name in any instruction file that still has the placeholder + local inst_file + for inst_file in AGENTS.md CLAUDE.md GEMINI.md; do + if grep -q "{{projectName}}" "$inst_file" 2>/dev/null; then + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s/{{projectName}}/$project_name/g" "$inst_file" + else + sed -i "s/{{projectName}}/$project_name/g" "$inst_file" + fi fi - fi + done echo "" if [ "$is_reinit" = true ]; then @@ -186,18 +180,23 @@ cmd_init() { else printf " ${GREEN}${ICON_SUCCESS}${NC} Context structure created\n" fi - printf " ${GRAY}CLAUDE.md .context/ .claude/commands/ (12) .claude/agents/ (12)${NC}\n" + local _names="" _i + for _i in $selected; do _names="$_names$(agent_name "$_i"), "; done + printf " ${GRAY}Harnesses: %s${NC}\n" "${_names%, }" echo "" - if [ "$skip_setup" = true ]; then - printf " ${CYAN}Next:${NC} run ${CYAN}/setup-context${NC} in Claude Code\n" - else - if command -v claude &> /dev/null; then + # /setup-context is a Claude command — only when Claude is part of the setup + if [ "$wants_claude" = true ]; then + if [ "$skip_setup" = true ]; then + printf " ${CYAN}Next:${NC} run ${CYAN}/setup-context${NC} in Claude Code\n" + elif command -v claude &> /dev/null; then printf " ${CYAN}Running /setup-context...${NC}\n\n" claude "/setup-context" else printf " ${YELLOW}Claude CLI not found.${NC} Run ${CYAN}/setup-context${NC} manually in Claude Code.\n" fi + else + printf " ${CYAN}Next:${NC} edit ${CYAN}AGENTS.md${NC} and ${CYAN}.context/CONTEXT.md${NC} to describe your project\n" fi echo "" } diff --git a/src/commands/update.sh b/src/commands/update.sh index cf1cc46..bca335c 100644 --- a/src/commands/update.sh +++ b/src/commands/update.sh @@ -148,6 +148,54 @@ cmd_update_templates() { print_green " Migrated remaining skills from .context/skills/ → .claude/skills/" fi + # Migration: adopt AGENTS.md as the canonical instruction source (ADR-016). + # Legacy projects have a content-bearing CLAUDE.md and no AGENTS.md. Moving + # the content to AGENTS.md (and leaving CLAUDE.md as an @import stub) is + # content- and behavior-preserving for Claude, and lets the other agents read + # the same instructions natively. + local _stub_comment='' + if [ -f "CLAUDE.md" ] && [ ! -f "AGENTS.md" ] && ! grep -q '@AGENTS.md' "CLAUDE.md" 2>/dev/null; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would migrate CLAUDE.md → AGENTS.md (+ CLAUDE.md import stub)" + else + local do_migrate=false + if [ "$auto_yes" = "true" ]; then + do_migrate=true + elif confirm_yes " Adopt AGENTS.md as the shared instructions file? (Claude keeps working via import)"; then + do_migrate=true + fi + if [ "$do_migrate" = true ]; then + mv "CLAUDE.md" "AGENTS.md" + printf '%s\n\n%s\n' "$_stub_comment" '@AGENTS.md' > "CLAUDE.md" + print_green " Migrated CLAUDE.md → AGENTS.md (Claude reads it via @import)" + fi + fi + fi + + # Add a Gemini import stub when the Gemini CLI is present and AGENTS.md exists. + if agent_detect gemini && [ -f "AGENTS.md" ] && [ ! -f "GEMINI.md" ]; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would add GEMINI.md (imports AGENTS.md)" + else + printf '%s\n\n%s\n' "$_stub_comment" '@AGENTS.md' > "GEMINI.md" + print_green " Added GEMINI.md (imports AGENTS.md)" + fi + fi + + # Mirror skills to .agents/skills/ when a non-Claude agent is present (ADR-017). + if [ -d ".claude/skills" ] && [ ! -e ".agents/skills" ]; then + local _na=false _aid + for _aid in $(detect_agents); do [ "$_aid" != "claude" ] && _na=true; done + if [ "$_na" = true ]; then + if [ "$dry_run" = "true" ]; then + print_gray " [dry-run] would mirror .claude/skills → .agents/skills (non-Claude agent detected)" + else + link_or_copy_dir ".claude/skills" ".agents/skills" + print_green " Mirrored skills to .agents/skills/" + fi + fi + fi + start_spinner "Checking templates..." # MANAGED templates: dotcontext-owned code (commands, templates that drive commands). @@ -184,6 +232,7 @@ cmd_update_templates() { # SEED templates: created once during init, customized by user or /setup-context. # Only added if missing — never offered for overwrite to protect user content. declare -a seed_templates=( + "templates/AGENTS.md:AGENTS.md" "templates/.context/decisions/README.md:.context/decisions/README.md" "templates/.claude/skills/bug-reproduction/SKILL.md:.claude/skills/bug-reproduction/SKILL.md" ) diff --git a/src/header.sh b/src/header.sh index fa5dda7..f8fcfc0 100644 --- a/src/header.sh +++ b/src/header.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -VERSION="0.15.0" +VERSION="0.16.0" REPO="goca-se/dotcontext" BRANCH="main" BASE_URL="https://raw.githubusercontent.com/${REPO}/${BRANCH}" diff --git a/src/setup/agents.sh b/src/setup/agents.sh new file mode 100644 index 0000000..25a76b7 --- /dev/null +++ b/src/setup/agents.sh @@ -0,0 +1,265 @@ +# ── Agent Adapter Registry ────────────────────────────────────────────────── +# Each supported coding agent is one entry. Adding an agent means adding it to +# AGENT_IDS and the case arms below — nothing else in the toolkit hard-codes an +# agent. (ADR-016) +# +# Emit modes for the project-instructions file: +# import — a thin stub that @-imports AGENTS.md (Claude Code, Gemini CLI) +# native — reads AGENTS.md directly; needs no file of its own (Codex, +# opencode, Copilot, Cursor incl. the cursor-agent CLI) +# +# AGENTS.md itself is the canonical single source and is always emitted. + +AGENT_IDS="claude codex opencode gemini copilot cursor" + +agent_name() { + case "$1" in + claude) echo "Claude Code" ;; + codex) echo "OpenAI Codex" ;; + opencode) echo "opencode" ;; + gemini) echo "Gemini CLI" ;; + copilot) echo "GitHub Copilot" ;; + cursor) echo "Cursor (cursor-agent)" ;; + *) echo "$1" ;; + esac +} + +# Detection: is the agent's CLI installed? +agent_detect() { + case "$1" in + claude) command -v claude >/dev/null 2>&1 ;; + codex) command -v codex >/dev/null 2>&1 ;; + opencode) command -v opencode >/dev/null 2>&1 ;; + gemini) command -v gemini >/dev/null 2>&1 ;; + copilot) command -v copilot >/dev/null 2>&1 || \ + { command -v gh >/dev/null 2>&1 && gh extension list 2>/dev/null | grep -qi copilot; } ;; + cursor) command -v cursor-agent >/dev/null 2>&1 ;; + *) return 1 ;; + esac +} + +# The instruction file this agent reads. +agent_instructions_file() { + case "$1" in + claude) echo "CLAUDE.md" ;; + gemini) echo "GEMINI.md" ;; + *) echo "AGENTS.md" ;; + esac +} + +# How this agent gets its instructions: import | native +agent_emit_mode() { + case "$1" in + claude|gemini) echo "import" ;; + *) echo "native" ;; + esac +} + +# Echo the ids of detected (installed) agents, space-separated. +detect_agents() { + local id out="" + for id in $AGENT_IDS; do + if agent_detect "$id"; then out="$out $id"; fi + done + echo "${out# }" +} + +# Echo "url|target" instruction-file seed mappings for the selected agent ids. +# AGENTS.md (canonical) is always first; import-mode agents add their stub. +# Native agents need no extra file (they read AGENTS.md directly). +# The "|" separator can't appear in a URL or a filename, so consumers parse it +# unambiguously: url="${mapping%|*}", target="${mapping##*|}". +agent_instruction_seeds() { + local selected="$1" id target seen=" " + echo "${BASE_URL}/templates/AGENTS.md|AGENTS.md" + for id in $selected; do + [ "$(agent_emit_mode "$id")" = "import" ] || continue + target="$(agent_instructions_file "$id")" + case "$seen" in *" $target "*) continue ;; esac + seen="$seen$target " + echo "${BASE_URL}/templates/${target}|${target}" + done +} + +# True if the id list contains a given agent. +agents_include() { + case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac +} + +# Resolve which harnesses to instantiate. Sets the global SELECTED_AGENTS. +# Priority: explicit --agents list > non-interactive (detected, fallback claude) +# > interactive (confirm each detected agent; offer Claude if none). +# Must NOT run inside $(...) — it prompts on stdout. (ADR-016) +SELECTED_AGENTS="" +resolve_selected_agents() { + local agents_flag="$1" skip_prompts="$2" + SELECTED_AGENTS="" + + if [ -n "$agents_flag" ]; then + local IFS=',' id + for id in $agents_flag; do + id="$(echo "$id" | tr -d '[:space:]')" + [ -z "$id" ] && continue + if agents_include "$AGENT_IDS" "$id"; then + SELECTED_AGENTS="$SELECTED_AGENTS $id" + else + print_yellow " unknown agent '$id' (skipped) — known: $AGENT_IDS" + fi + done + SELECTED_AGENTS="${SELECTED_AGENTS# }" + return 0 + fi + + local detected; detected="$(detect_agents)" + + if [ "$skip_prompts" = "true" ]; then + SELECTED_AGENTS="$detected" + [ -z "$SELECTED_AGENTS" ] && SELECTED_AGENTS="claude" + return 0 + fi + + local id + if [ -n "$detected" ]; then + print_blue "Detected agents — choose which to set up:" + for id in $detected; do + if confirm_yes " Set up for $(agent_name "$id")?"; then + SELECTED_AGENTS="$SELECTED_AGENTS $id" + fi + done + fi + SELECTED_AGENTS="${SELECTED_AGENTS# }" + + if [ -z "$SELECTED_AGENTS" ]; then + if confirm_yes " No agent selected — set up for Claude Code?"; then + SELECTED_AGENTS="claude" + fi + fi +} + +# Create $2 as a symlink to $1 (relative), or copy if symlinks aren't available. +# No-op if $2 already exists. Used to mirror the skills dir across ecosystems. +link_or_copy_dir() { + local src="$1" dst="$2" + [ -d "$src" ] || return 0 + if [ -e "$dst" ] || [ -L "$dst" ]; then return 0; fi + mkdir -p "$(dirname "$dst")" + ln -s "../$src" "$dst" 2>/dev/null || cp -r "$src" "$dst" +} + +# ── Per-agent hooks ─────────────────────────────────────────────────────────── +# Wire a "task finished / needs attention" notification per selected agent. +# Claude keeps its richer hooks (notify + tool-failure-guard) via +# setup_notifications. Non-Claude agents get a notification hook in their native +# config format, pointing at a shared, arg-driven notify.sh (it ignores stdin, +# so the same script works regardless of each agent's hook JSON contract). +# Best-effort + create-only: never clobbers an existing agent config. +NOTIFY_SHARED=".context/scripts/notify.sh" + +ensure_shared_notify() { + [ -f "$NOTIFY_SHARED" ] && return 0 + mkdir -p ".context/scripts" + download "${BASE_URL}/scripts/notify.sh" "$NOTIFY_SHARED" && chmod +x "$NOTIFY_SHARED" +} + +setup_agent_hooks() { + local selected="$1" id done_any=false + for id in $selected; do + case "$id" in + claude) + setup_notifications 2>/dev/null || true + done_any=true + ;; + codex) + ensure_shared_notify + if [ ! -f ".codex/hooks.json" ]; then + mkdir -p ".codex" + cat > ".codex/hooks.json" < ".gemini/settings.json" < ".github/hooks/dotcontext-notify.json" < ".cursor/hooks.json" < ".opencode/plugins/dotcontext-notify.js" <<'JS' +// dotcontext: fire an OS notification when the session goes idle. +export const DotcontextNotify = async ({ $ }) => ({ + event: async ({ event }) => { + if (event?.type === "session.idle") { + await $`.context/scripts/notify.sh 'opencode' 'Task completed' success`.catch(() => {}) + } + }, +}) +JS + print_gray " hooks: .opencode/plugins/dotcontext-notify.js" + done_any=true + fi + ;; + esac + done + [ "$done_any" = true ] || true +} diff --git a/templates/AGENTS.md b/templates/AGENTS.md new file mode 100644 index 0000000..a0584a0 --- /dev/null +++ b/templates/AGENTS.md @@ -0,0 +1,100 @@ +# {{projectName}} + +> [Project description - one line] + + + +## Decision Compliance + +**IMPORTANT:** Before implementing any change, check `.context/decisions/` for related ADRs. + +If a requested change conflicts with an existing decision: +1. **Stop and inform the user** which ADR(s) would be affected +2. **Ask explicitly** if they want to: + - Proceed and update the decision + - Modify the approach to comply with existing decision + - Cancel the change +3. **If updating a decision**, create a new version: + - Change status to `Superseded by ADR-XXX` + - Create new ADR with updated decision + - Reference the previous ADR + +## Stack + +- [Language and version] +- [Framework] +- [Database] +- [Other major dependencies] + +## Commands + +**Important:** Check if this project uses Docker (docker-compose.yml, Dockerfile). If so, run commands via Docker (e.g., `docker compose exec app npm test` instead of `npm test`). + +```bash +# Development +# [dev command] + +# Testing +# [test command] + +# Linting +# [lint command] + +# Build/Deploy +# [build command] +``` + +## Critical Rules + +1. **Always ask before assuming** - When there is ambiguity, multiple valid approaches, or decisions to be made, ask the user to clarify before proceeding (use your interactive question mechanism if you have one). Never assume user intent. +2. **[Rule 1]** - [Why it matters] +3. **[Rule 2]** - [Why it matters] +4. **[Rule 3]** - [Why it matters] + +## Architecture + +### [Section 1] + +[Brief description of key architectural pattern] + +### [Section 2] + +[Brief description of another key pattern] + +## Efficiency Rules + +- **Read before changing** — Always read a file before editing it. Never modify code based on assumptions about its content. +- **Follow existing patterns** — Before implementing something new, look at how similar things are done in the codebase. Match the existing style, conventions, and patterns. +- **Scope reads to the task** — Only read files directly relevant to the change. Do not explore broadly before acting on focused tasks. +- **Load context progressively** — Start with the minimum files needed. Only expand to related files when the current context is insufficient to complete the task. +- **Code only** — When implementing changes, output code. Skip explanations, preamble, and commentary unless the user asks for them. +- **Skip summaries** — After making changes, do not summarize what you did unless asked. Show `git diff` instead. +- **Run targeted tests** — After a change, run only tests related to the modified files. Only run the full suite when asked or before committing. +- **Never read generated files** — Do not read lock files, build output, vendored dependencies, or source maps. These are listed in `.claudeignore`. + +## Compact Instructions + +When compacting, preserve: +- Test results and error output +- File paths and code changes made +- Key decisions and their rationale + +Remove: +- Exploratory file reads that did not lead to changes +- Verbose command output that has been summarized +- Discussion of rejected approaches + +--- + +## Additional Context + +- Domain and architecture → `.context/CONTEXT.md` +- Architectural decisions → `.context/decisions/` +- Task-specific skills → `.claude/skills/` +- Bug reproduction guide → `.claude/skills/bug-reproduction/SKILL.md` +- Batch operations guide → `.claude/skills/batch-operations/SKILL.md` +- Git platform detection → `.claude/skills/git-platform/SKILL.md` diff --git a/templates/CLAUDE.md b/templates/CLAUDE.md index f367744..74757a0 100644 --- a/templates/CLAUDE.md +++ b/templates/CLAUDE.md @@ -1,94 +1,8 @@ # {{projectName}} -> [Project description - one line] + -## Decision Compliance - -**IMPORTANT:** Before implementing any change, check `.context/decisions/` for related ADRs. - -If a requested change conflicts with an existing decision: -1. **Stop and inform the user** which ADR(s) would be affected -2. **Ask explicitly** if they want to: - - Proceed and update the decision - - Modify the approach to comply with existing decision - - Cancel the change -3. **If updating a decision**, create a new version: - - Change status to `Superseded by ADR-XXX` - - Create new ADR with updated decision - - Reference the previous ADR - -## Stack - -- [Language and version] -- [Framework] -- [Database] -- [Other major dependencies] - -## Commands - -**Important:** Check if this project uses Docker (docker-compose.yml, Dockerfile). If so, run commands via Docker (e.g., `docker compose exec app npm test` instead of `npm test`). - -```bash -# Development -# [dev command] - -# Testing -# [test command] - -# Linting -# [lint command] - -# Build/Deploy -# [build command] -``` - -## Critical Rules - -1. **Always ask before assuming** - When there is ambiguity, multiple valid approaches, or decisions to be made, use the AskUserQuestion tool to clarify before proceeding. Never assume user intent. -2. **[Rule 1]** - [Why it matters] -3. **[Rule 2]** - [Why it matters] -4. **[Rule 3]** - [Why it matters] - -## Architecture - -### [Section 1] - -[Brief description of key architectural pattern] - -### [Section 2] - -[Brief description of another key pattern] - -## Efficiency Rules - -- **Read before changing** — Always read a file before editing it. Never modify code based on assumptions about its content. -- **Follow existing patterns** — Before implementing something new, look at how similar things are done in the codebase. Match the existing style, conventions, and patterns. -- **Scope reads to the task** — Only read files directly relevant to the change. Do not explore broadly before acting on focused tasks. -- **Load context progressively** — Start with the minimum files needed. Only expand to related files when the current context is insufficient to complete the task. -- **Code only** — When implementing changes, output code. Skip explanations, preamble, and commentary unless the user asks for them. -- **Skip summaries** — After making changes, do not summarize what you did unless asked. Show `git diff` instead. -- **Run targeted tests** — After a change, run only tests related to the modified files. Only run the full suite when asked or before committing. -- **Never read generated files** — Do not read lock files, build output, vendored dependencies, or source maps. These are listed in `.claudeignore`. - -## Compact Instructions - -When compacting, preserve: -- Test results and error output -- File paths and code changes made -- Key decisions and their rationale - -Remove: -- Exploratory file reads that did not lead to changes -- Verbose command output that has been summarized -- Discussion of rejected approaches - ---- - -## Additional Context - -- Domain and architecture → `.context/CONTEXT.md` -- Architectural decisions → `.context/decisions/` -- Task-specific skills → `.claude/skills/` -- Bug reproduction guide → `.claude/skills/bug-reproduction/SKILL.md` -- Batch operations guide → `.claude/skills/batch-operations/SKILL.md` -- Git platform detection → `.claude/skills/git-platform/SKILL.md` +@AGENTS.md diff --git a/templates/GEMINI.md b/templates/GEMINI.md new file mode 100644 index 0000000..6328b6d --- /dev/null +++ b/templates/GEMINI.md @@ -0,0 +1,8 @@ +# {{projectName}} + + + +@AGENTS.md