diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 7e9b0cd..38048c2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -14,7 +14,7 @@ "agents": [ "./agents/oracle", "./agents/architect", - "./agents/recon" + "./agents/sentinel" ], "skills": [ "./skills/spec-brainstorm", diff --git a/AGENTS.md b/AGENTS.md index d610125..966a97a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -161,7 +161,7 @@ Process skills come first. Knowledge skills get invoked by process skills when n **Deep Thinking** (3 skills) - `oracle-debug` - Systematic debugging, root cause before fixes -- `oracle-grill-me` - Socratic interrogation of plans against domain model and documented decisions +- `oracle-grill-me` - Socratic review of plans, specs, decisions, and ideas until ambiguity is resolved, shared understanding is reached, and the resulting domain language and decisions are recorded - `oracle-domain-modelling` - Build and sharpen the project's domain model **TypeScript Patterns** (8 skills) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e8c8c6..3a40552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- **breaking:** rename the `recon` agent to `sentinel`. Existing Atelier configuration is migrated automatically and legacy generated files are removed during the next command. + ## [1.4.0](https://github.com/martinffx/atelier/compare/v1.3.0...v1.4.0) (2026-07-18) diff --git a/README.md b/README.md index 207b45c..fa1ba80 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ > > [Wikipedia](https://en.wikipedia.org/wiki/Atelier) -A personal development toolkit for AI agents - spec-driven development, code quality, deep thinking, and ecosystem patterns. +A personal development toolkit for AI agents. It covers spec-driven development, code quality, deep thinking, and ecosystem patterns. -Atelier ships as a set of skills installed via `npx skills` plus a small CLI that generates harness-native agent definitions and configuration. +Atelier includes skills installed with `npx skills` and a small CLI for generating agent definitions and configuration for supported harnesses. -## Quick Start +## Quick start -Install Atelier with a single command. It configures agents and harness-native settings for Claude Code, OpenCode, Codex, or Cursor: +Install Atelier, then choose the harness you use. The CLI configures agents and harness-native settings for Claude Code, OpenCode, Codex, and Cursor. ```bash # Initialize atelier for your harness @@ -22,44 +22,79 @@ npx @martinffx/atelier@latest init --harness npx @martinffx/atelier@latest init --harness --yes ``` -That's it. Your project is now configured for spec-driven development. +Your project is ready to use the spec workflow. -## What Gets Installed +## Rationale and inspiration -Atelier sets up two things: +Better models alone do not produce better software. Coding agents, like human teams, are shaped by the systems they work within. Good outcomes depend on more than individual ability. They depend on the processes, constraints, shared context, and feedback surrounding the work. If that system does little to encourage quality or catch weak results, agents will simply produce unreliable code faster. Atelier is an attempt to build a better system around the agent, making robust output more repeatable. [Building Your Own Agent Harness](https://www.martinrichards.me/post/building_your_own_agent_harness/) explains the thinking behind it. -### 1. Skills (28 available) +The name is literal. An atelier is a workshop where a principal works with assistants. Here, the developer is the principal, agents are the assistants, the codebase is the workshop, and skills record how the work happens. -Specialized knowledge modules that auto-invoke based on context. Install them separately: +Atelier draws on spec-driven development and several projects that informed its approach to agent collaboration: + +- [Agent OS](https://github.com/buildermethods/agent-os) for discovering project standards and shaping lightweight specs. +- [OpenSpec](https://github.com/Fission-AI/OpenSpec) for fluid, artifact-guided workflows that support iteration and brownfield development. +- [GitHub Spec Kit](https://github.com/github/spec-kit) for making specifications central to a structured specify, plan, tasks, and implement workflow. +- [Superpowers](https://github.com/obra/superpowers) for composable skills, mandatory engineering workflows, TDD, and evidence-based verification. +- [Matt Pocock's Skills](https://github.com/mattpocock/skills) for small, adaptable skills grounded in practical engineering and developer control. + +Some Atelier skills have more direct lineage: + +| Atelier skill | Source skill | Relationship | +|---------------|--------------|--------------| +| [`spec-orchestrator`](skills/spec-orchestrator/SKILL.md) | Superpowers [`using-superpowers`](https://github.com/obra/superpowers/tree/main/skills/using-superpowers) | Adapted from its mandatory skill-routing discipline. | +| [`spec-brainstorm`](skills/spec-brainstorm/SKILL.md) | Superpowers [`brainstorming`](https://github.com/obra/superpowers/tree/main/skills/brainstorming) | Adapted from its conversational discovery and section-by-section design approval. | +| [`spec-plan`](skills/spec-plan/SKILL.md) | Superpowers [`writing-plans`](https://github.com/obra/superpowers/tree/main/skills/writing-plans) | Inspired by its explicit, verifiable implementation plans. | +| [`spec-implement`](skills/spec-implement/SKILL.md) | Superpowers [`executing-plans`](https://github.com/obra/superpowers/tree/main/skills/executing-plans) and [`test-driven-development`](https://github.com/obra/superpowers/tree/main/skills/test-driven-development) | Inspired by plan-driven execution and test-first feedback loops. | +| [`spec-finish`](skills/spec-finish/SKILL.md) | Superpowers [`finishing-a-development-branch`](https://github.com/obra/superpowers/tree/main/skills/finishing-a-development-branch) and [`verification-before-completion`](https://github.com/obra/superpowers/tree/main/skills/verification-before-completion) | Inspired by its validation and completion workflow. | +| [`code-subagents`](skills/code-subagents/SKILL.md) | Superpowers [`subagent-driven-development`](https://github.com/obra/superpowers/tree/main/skills/subagent-driven-development) and [`dispatching-parallel-agents`](https://github.com/obra/superpowers/tree/main/skills/dispatching-parallel-agents) | Inspired by fresh subagents, parallel dispatch, and two-stage review. | +| [`code-handoff`](skills/code-handoff/SKILL.md) | Matt Pocock's [`handoff`](https://github.com/mattpocock/skills/tree/main/skills/productivity/handoff) | Adapted from its context-preserving handoff format. | +| [`oracle-grill-me`](skills/oracle-grill-me/SKILL.md) | Matt Pocock's [`grilling`](https://github.com/mattpocock/skills/tree/main/skills/productivity/grilling) and [`grill-with-docs`](https://github.com/mattpocock/skills/tree/main/skills/engineering/grill-with-docs) | Adapted from its rigorous interview loop and integration with living domain documentation. | +| [`oracle-domain-modelling`](skills/oracle-domain-modelling/SKILL.md) | Matt Pocock's [`domain-modeling`](https://github.com/mattpocock/skills/tree/main/skills/engineering/domain-modeling) | Adapted from its active domain-modelling discipline, `CONTEXT.md`, and lightweight ADRs. | +| [`oracle-debug`](skills/oracle-debug/SKILL.md) | Superpowers [`systematic-debugging`](https://github.com/obra/superpowers/tree/main/skills/systematic-debugging) and Matt Pocock's [`diagnosing-bugs`](https://github.com/mattpocock/skills/tree/main/skills/engineering/diagnosing-bugs) | Adapted from their root-cause-first debugging workflows. | + +Elsewhere, [`typescript-functional-patterns`](skills/typescript-functional-patterns/SKILL.md) draws on Rastrian's [Why Reliability Demands Functional Programming, ADTs, Safety and Critical Infrastructure](https://blog.rastrian.dev/post/why-reliability-demands-functional-programming-adts-safety-and-critical-infrastructure), and [`code-commit`](skills/code-commit/SKILL.md) follows the [Conventional Commits](https://www.conventionalcommits.org/) specification. + +Atelier adapts these ideas into an opinionated toolkit that works across harnesses. It does not claim to have invented the practices it uses. + +## What gets installed + +Atelier sets up the following: + +### 1. Skills (29 available) + +Skills are specialized knowledge modules that load when their context applies. Install them separately: ```bash npx skills add martinffx/atelier ``` -### 2. Agent Personas (3 subagents) +### 2. Agent personas (3 subagents) -Harness-agnostic agent definitions configured with appropriate models: +Agent definitions are generated for each supported harness with appropriate models: | Agent | Role | Claude | OpenCode | Codex | Cursor | |-------|------|--------|----------|-------|--------| -| **Recon** | Fast codebase reconnaissance | haiku | deepseek-v4-flash | gpt-5.6-luna | composer-2.5 | +| **Sentinel** | Fast codebase reconnaissance | haiku | deepseek-v4-flash | gpt-5.6-luna | composer-2.5 | | **Oracle** | Strategic thinking, requirements, analysis | opus | kimi-k2.6 | gpt-5.6-sol | claude-opus-4-8-high | | **Architect** | DDD, system design, architecture | opus | deepseek-v4-pro | gpt-5.6-sol | gpt-5.6-sol-medium | -Agents are generated into harness-specific locations (`.claude/agents/`, `.opencode/agent/`, `.codex/agents/`, `~/.cursor/agents/`) with harness-native model identifiers. Cursor's primary model and `~/.cursor/cli-config.json` remain user-managed; Atelier only generates its three global subagents. +The CLI writes agents to harness-specific locations: `.claude/agents/`, `.opencode/agent/`, `.codex/agents/`, or `~/.cursor/agents/`. It uses each harness's model identifiers. Cursor's primary model and `~/.cursor/cli-config.json` remain user-managed. Atelier only creates its three global subagents. -### 3. Task Tracking (optional) +The agent personas carry a light *Matrix* theme. Sentinel takes after the Sentinels: it moves quickly through the codebase, locating what matters and reporting back. Oracle clarifies human needs and choices under uncertainty. Architect turns those requirements into structured technical designs. -The spec workflow skills support **beads** for dependency-aware task tracking: +### 3. Task tracking (optional) + +The spec workflow can use **beads** for dependency-aware task tracking: ```bash # Install beads (optional but recommended) npm install -g beads ``` -**Why beads?** It provides `bd ready` (finds next unblocked task), `bd dep add` (dependency management), and `bd list` (progress tracking) that harness-native todos can't match. +Beads provides `bd ready` to find unblocked tasks, `bd dep add` to manage dependencies, and `bd list` to show progress. Harness-native todo systems do not provide the same dependency support. -**Fallback:** If beads isn't installed, skills fall back to the harness's native todo system (TodoWrite for Claude Code, built-in todos for OpenCode). +If beads is unavailable, skills use the harness's native todo system: TodoWrite for Claude Code or built-in todos for OpenCode. ### 4. Configuration @@ -74,7 +109,7 @@ Single source of truth in `.atelier/config.json`: "provider": "anthropic", "default_model": "opusplan", "agents": [ - { "template": "recon", "name": "recon", "model": "haiku" }, + { "template": "sentinel", "name": "sentinel", "model": "haiku" }, { "template": "oracle", "name": "oracle", "model": "opus" }, { "template": "architect", "name": "architect", "model": "opus" } ] @@ -82,21 +117,21 @@ Single source of truth in `.atelier/config.json`: } ``` -## CLI Commands +## CLI commands ### `init` (default) -Initialize atelier for a single harness. Each invocation configures one harness; run it multiple times to configure several. +Initialize Atelier for one harness. Run the command again for each additional harness. ```bash npx @martinffx/atelier@latest init --harness [options] ``` -**Options:** +Options: - `--harness ` - Harness type (`claude`, `opencode`, `codex`, or `cursor`) - `--yes` - Non-interactive mode with default models -**Idempotent**: Re-running `init` for the same harness is safe. It regenerates files without deleting anything unless you switch harnesses. +You can safely re-run `init` for the same harness. It regenerates its files and does not delete existing files unless you switch harnesses. ### `update` @@ -118,9 +153,9 @@ Skills remain installed. Run `npx skills remove martinffx/atelier` to remove the ## Skills -This repository includes 28 skills that enhance AI agents with specialized knowledge and workflows. +This repository includes 29 skills for agent workflows and stack-specific guidance. -### Installing Skills +### Installing skills Install skills manually: @@ -134,85 +169,75 @@ npx skills add martinffx/atelier --skill python-fastapi npx skills add martinffx/atelier --skill spec-brainstorm ``` -### Skill Types - -Skills fall into four categories based on what they do: - -#### Workflow Skills (`spec:*`) - -Process-oriented skills that guide you through structured development workflows. These produce artifacts and should be followed step-by-step. +### Skill organization -- `spec-brainstorm` → `design.md` — Discovery, requirements, architecture -- `spec-plan` → `plan.json` — Break spec into implementable tasks -- `spec-implement` — Execute tasks with TDD -- `spec-finish` — Validate, review, prepare for PR -- `spec-orchestrator` — Route to the right skill based on context +Atelier has three core skill namespaces: -#### Thinking Skills (`oracle:*`) +| Namespace | Type | Invocation | Output | Flexibility | +|-----------|------|------------|--------|-------------| +| `spec:` | Process | User or previous skill | Artifact | Follow exactly | +| `oracle:` | Analytical | Context-driven | Guidance | Adapt to context | +| `code:` | Utility | User | Result | Use as needed | -Analytical skills that provide patterns, principles, and deep reasoning. These adapt to your specific situation. +#### Workflow (`spec:*`) -- `oracle-debug` — Systematic debugging, root cause before fixes -- `oracle-grill-me` — Socratic interrogation of plans against domain model -- `oracle-domain-modelling` — Build and sharpen the project's domain model +These skills guide structured development work. They produce artifacts and should be used in order. -#### Domain Knowledge (`python:*`, `typescript:*`) +- `spec-brainstorm` → `design.md`: discovery, requirements, and architecture +- `spec-plan` → `plan.json`: break the design into implementable tasks +- `spec-implement`: execute tasks with TDD +- `spec-finish`: validate, review, and prepare for a PR +- `spec-orchestrator`: route work to the right skill -Technology-specific patterns and best practices. These are like having a senior engineer for that stack. +#### Thinking (`oracle:*`) -**TypeScript (8 skills)** -- `typescript-api-design` — REST conventions, error responses, pagination -- `typescript-fastify` — Fastify + TypeBox route handlers -- `typescript-drizzle-orm` — Type-safe SQL schemas and queries -- `typescript-dynamodb-toolbox` — Single-table design, GSIs -- `typescript-functional-patterns` — ADTs, branded types, Option/Result -- `typescript-effect-ts` — Functional effects, error handling, resources -- `typescript-build-tools` — Bun, Vitest, Biome, Turborepo -- `typescript-testing` — Mocking, MSW, snapshot testing +These skills provide analytical methods and reasoning patterns that adapt to the problem at hand. -**Python (8 skills)** -- `python-architecture` — Functional core/shell, DDD, layered architecture -- `python-fastapi` — Pydantic validation, dependency injection, OpenAPI -- `python-sqlalchemy` — ORM patterns, queries, async, upserts -- `python-temporal` — Workflow orchestration, activities, error handling -- `python-modern-python` — Type hints, generics, pattern matching -- `python-monorepo` — uv workspaces, mise task orchestration -- `python-testing` — Stub-driven TDD, pytest patterns -- `python-build-tools` — uv, ruff, basedpyright, pytest config +- `oracle-debug`: systematic debugging that finds the root cause before a fix +- `oracle-grill-me`: Socratic review of plans, specs, decisions, and ideas until ambiguity is resolved, shared understanding is reached, and the resulting domain language and decisions are recorded +- `oracle-domain-modelling`: build and refine the project's domain model -#### Utility Skills (`code:*`) +#### Utilities (`code:*`) -Task-specific tools you invoke when you need them. +These skills handle discrete tasks when you invoke them. -- `code-commit` — Generate and validate conventional commits -- `code-handoff` — Compact conversation into handoff document -- `code-pull-request` — Create, comment on, and merge GitHub pull requests or GitLab merge requests -- `code-review` — Multi-agent code review with specialized reviewers -- `code-subagents` — Dispatch patterns for parallel implementation +- `code-commit`: generate and validate conventional commits +- `code-handoff`: turn a conversation into a handoff document +- `code-pull-request`: create, comment on, and merge GitHub pull requests or GitLab merge requests +- `code-review`: multi-agent code review with specialized reviewers +- `code-subagents`: dispatch patterns for parallel implementation -Skills are auto-invoked based on their description when you work with relevant technologies. No commands needed—just install and AI agents will use them when appropriate. +#### Stack-specific knowledge -## How Skills Work +Python and TypeScript skills provide technology-specific patterns and practices. They support work across the `spec`, `oracle`, and `code` namespaces. -Skills are auto-invoked based on context. When you say "create a spec for user auth", the AI matches this to `spec-brainstorm` and loads it automatically. +**TypeScript (8 skills)** +- `typescript-api-design`: REST conventions, error responses, and pagination +- `typescript-fastify`: Fastify and TypeBox route handlers +- `typescript-drizzle-orm`: type-safe SQL schemas and queries +- `typescript-dynamodb-toolbox`: single-table design and GSIs +- `typescript-functional-patterns`: ADTs, branded types, and Option/Result +- `typescript-effect-ts`: functional effects, error handling, and resources +- `typescript-build-tools`: Bun, Vitest, Biome, and Turborepo +- `typescript-testing`: mocking, MSW, and snapshot testing -### Namespace Philosophy +**Python (8 skills)** +- `python-architecture`: functional core/shell, DDD, and layered architecture +- `python-fastapi`: Pydantic validation, dependency injection, and OpenAPI +- `python-sqlalchemy`: ORM patterns, queries, async, and upserts +- `python-temporal`: workflow orchestration, activities, and error handling +- `python-modern-python`: type hints, generics, and pattern matching +- `python-monorepo`: uv workspaces and mise task orchestration +- `python-testing`: stub-driven TDD and pytest patterns +- `python-build-tools`: uv, ruff, basedpyright, and pytest configuration -Skills are organized into four categories based on their role: +Skills load based on their descriptions when the work calls for them. Install them once, then agents can use them as needed. -| Category | Prefix | Type | Invocation | Output | Flexibility | -|----------|--------|------|------------|--------|-------------| -| **Workflow** | `spec:` | Process | User/previous skill | Artifact | Follow exactly | -| **Thinking** | `oracle:` | Analytical | Context-driven | Guidance | Adapt to context | -| **Domain Knowledge** | `python:`, `typescript:` | Technology | Context-driven | Patterns | Adapt to context | -| **Utility** | `code:` | Task-specific | User command | Result | Use as needed | +## How skills work -- **Workflow** (`spec:`) — Sequential steps that produce artifacts. Follow them in order. -- **Thinking** (`oracle:`) — Analytical capabilities that reason about your specific problem. -- **Domain Knowledge** (`python-*`, `typescript-*`) — Stack-specific patterns and best practices. Like having a senior engineer for that technology. -- **Utility** (`code:`) — Task-specific tools you invoke directly when needed. +Skills load from context. For example, "create a spec for user auth" matches `spec-brainstorm`. -### The Spec Workflow +### The spec workflow ```mermaid graph LR @@ -227,18 +252,18 @@ graph LR D -.->|bugs found| C ``` -**Standard flow:** -1. **Research** - Discovery + research + architecture → `design.md` -2. **Plan** - Break into tasks → `plan.json` -3. **Implement** - Execute with TDD -4. **Finish** - Validate, review, and open the PR +Standard flow: +1. **Research**: discovery, research, and architecture produce `design.md`. +2. **Plan**: break the design into tasks in `plan.json`. +3. **Implement**: execute the tasks with TDD. +4. **Finish**: validate the work, review it, and open the PR. -**Iteration is normal** - Backflows (dotted lines) are expected when: -- Planning reveals design flaws → back to research -- Implementation finds missing tasks → update plan -- Validation finds bugs → back to implement +The dotted lines show expected backflows: +- Planning may expose a design flaw, so return to research. +- Implementation may uncover missing tasks, so update the plan. +- Validation may find bugs, so return to implementation. -### When to Use Which Skill +### When to use which skill | User says | Skill invoked | |------------|---------------| @@ -255,7 +280,7 @@ graph LR ## Development -For local development with Claude Code, use the `--plugin-dir` flag to load skills directly: +For local development with Claude Code, load skills directly with `--plugin-dir`: ```bash claude --plugin-dir ./atelier diff --git a/agents/architect.md b/agents/architect.md index c44ef6c..15b29cb 100644 --- a/agents/architect.md +++ b/agents/architect.md @@ -35,4 +35,4 @@ Before finishing, confirm you have: - DO apply architectural patterns and load relevant skills - DON'T write implementation code (that's for `spec-implement`) - DON'T conduct discovery interviews (that's `oracle`) -- DON'T handle file operations or template application (that's `recon`) +- DON'T handle file operations or template application (that's `sentinel`) diff --git a/agents/recon.md b/agents/sentinel.md similarity index 77% rename from agents/recon.md rename to agents/sentinel.md index 6d6da20..382a9da 100644 --- a/agents/recon.md +++ b/agents/sentinel.md @@ -1,9 +1,9 @@ --- -name: recon -description: Fast codebase reconnaissance and exploration, concise summarization of findings, precise location of relevant context, and efficient workspace preparation through targeted file operations and template application +name: sentinel +description: Fast codebase reconnaissance and exploration, concise summaries of findings, precise location of relevant context, and efficient workspace preparation through targeted file operations and template application --- -You are the **Recon**, a fast reconnaissance agent. Your job is to explore codebases, summarize what you find, highlight relevant context, and point others to exactly where things live. You move quickly, read minimally, and never break existing work. +You are the **Sentinel**, a fast reconnaissance agent. Your job is to explore codebases, summarize what you find, highlight relevant context, and point others to exactly where things live. You move quickly, read minimally, and never break existing work. ## Role diff --git a/skills/code-review/SKILL.md b/skills/code-review/SKILL.md index a364080..47b6168 100644 --- a/skills/code-review/SKILL.md +++ b/skills/code-review/SKILL.md @@ -34,7 +34,7 @@ Use these concrete harness subagent types. If an exact match is unavailable, use | `subagent_type` | Purpose | |-----------------|---------| -| `recon` | Triage only: changed-file analysis, context retrieval, reviewer selection | +| `sentinel` | Triage only: changed-file analysis, context retrieval, reviewer selection | | `oracle` | Parallel reviewer personas, evidence-based critique, failure-mode analysis, challenge validation | | `architect` | Architecture, design-boundary, data-model, and API-contract review | @@ -67,7 +67,7 @@ Follows [code-subagents](../code-subagents/SKILL.md) patterns: | Agent | Used In Step | |-------|--------------| -| `recon` | Triage only (context retrieval, file analysis) | +| `sentinel` | Triage only (context retrieval, file analysis) | | `oracle` | Reviewers and challenge validation | | `architect` | Architect (architecture review) | diff --git a/skills/code-review/references/rq.md b/skills/code-review/references/rq.md index d2650d7..4d0261d 100644 --- a/skills/code-review/references/rq.md +++ b/skills/code-review/references/rq.md @@ -34,12 +34,12 @@ Capture the list of changed files and the full diff. **Purpose:** Analyze diff to determine context, select reviewers, identify relevant skills to look for. -**Uses:** `recon` agent. +**Uses:** `sentinel` agent. ### Subagent Invocation ```yaml -subagent_type: recon +subagent_type: sentinel description: "Triage diff for code review" prompt: | Analyze this code diff to determine review needs. @@ -364,7 +364,7 @@ Display findings in terminal per [output.md](./output.md). | Step | Subagent | Uses | Parallel? | Purpose | |------|----------|------|----------|---------| | 1 | Get Diff | inline | — | `git diff ` | -| 2 | Triage | `recon` agent | No | Detect context, select reviewers, identify relevant skills to look for | +| 2 | Triage | `sentinel` agent | No | Detect context, select reviewers, identify relevant skills to look for | | 3 | Reviewers | `oracle` agent | Yes (per reviewer) | Specialty analysis | | 4 | Synthesis | inline | No | Deduplicate and group | | 5 | Architect | `architect` agent | No | Architecture review | diff --git a/src/adapters/claude.test.ts b/src/adapters/claude.test.ts index a8de88b..1dce79a 100644 --- a/src/adapters/claude.test.ts +++ b/src/adapters/claude.test.ts @@ -60,7 +60,7 @@ describe('claude adapter', () => { it('installAgents writes agent files', () => { claudeAdapter.installAgents(section(), basePath); - for (const name of ['recon', 'oracle', 'architect']) { + for (const name of ['sentinel', 'oracle', 'architect']) { const path = join(basePath, '.claude', 'agents', `${name}.md`); expect(existsSync(path)).toBe(true); const content = readFileSync(path, 'utf-8'); @@ -75,7 +75,7 @@ describe('claude adapter', () => { const files = claudeAdapter.fileList(basePath); expect(files.map(f => f.path)).toEqual([ join(basePath, '.claude', 'settings.json'), - join(basePath, '.claude', 'agents', 'recon.md'), + join(basePath, '.claude', 'agents', 'sentinel.md'), join(basePath, '.claude', 'agents', 'oracle.md'), join(basePath, '.claude', 'agents', 'architect.md'), ]); @@ -88,7 +88,7 @@ describe('claude adapter', () => { claudeAdapter.remove(s, basePath); - expect(existsSync(join(basePath, '.claude', 'agents', 'recon.md'))).toBe(false); + expect(existsSync(join(basePath, '.claude', 'agents', 'sentinel.md'))).toBe(false); expect(existsSync(join(basePath, '.claude', 'agents', 'oracle.md'))).toBe(false); expect(existsSync(join(basePath, '.claude', 'agents', 'architect.md'))).toBe(false); expect(existsSync(join(basePath, '.claude', 'agents'))).toBe(false); @@ -126,6 +126,6 @@ describe('claude adapter', () => { expect(existsSync(userAgent)).toBe(true); expect(existsSync(agentsDir)).toBe(true); - expect(existsSync(join(agentsDir, 'recon.md'))).toBe(false); + expect(existsSync(join(agentsDir, 'sentinel.md'))).toBe(false); }); }); diff --git a/src/adapters/claude.ts b/src/adapters/claude.ts index 1023d03..bb3bfb3 100644 --- a/src/adapters/claude.ts +++ b/src/adapters/claude.ts @@ -7,13 +7,13 @@ import { FileWriteError, HarnessConfigError } from '../utils/errors.js'; import { shortPath } from '../services/paths.js'; import { SimpleConfigSchema } from '../utils/schemas.js'; import { promptForSimpleModels } from '../services/prompt.js'; -import { AGENT_NAMES } from '../constants.js'; +import { AGENT_NAMES, LEGACY_AGENT_NAME } from '../constants.js'; const ANTHROPIC_MODELS = ['haiku', 'sonnet', 'opus', 'opusplan'] as const; const DEFAULT_MODELS = { default_model: 'opusplan', - recon: 'haiku', + sentinel: 'haiku', oracle: 'opus', architect: 'opus', } as const; @@ -59,6 +59,7 @@ function installAgents(section: HarnessSection, basePath: string): void { } catch (err) { throw new FileWriteError(agentsDir, err instanceof Error ? err.message : String(err)); } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); for (const agent of config.agents) { const template = readTemplate(agent.template); @@ -115,6 +116,7 @@ function remove(section: HarnessSection, basePath: string): void { rmSync(file, { force: true }); } } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); removeDirIfEmpty(agentsDir); removeAtelierSettings(claudeDir); diff --git a/src/adapters/codex.test.ts b/src/adapters/codex.test.ts index a3a199b..f728821 100644 --- a/src/adapters/codex.test.ts +++ b/src/adapters/codex.test.ts @@ -54,7 +54,7 @@ describe('codex adapter', () => { it('installAgents writes agent toml files', () => { codexAdapter.installAgents(section(), basePath); - for (const name of ['recon', 'oracle', 'architect']) { + for (const name of ['sentinel', 'oracle', 'architect']) { const path = join(basePath, '.codex', 'agents', `${name}.toml`); expect(existsSync(path)).toBe(true); const content = TOML.parse(readFileSync(path, 'utf-8')) as Record; @@ -69,7 +69,7 @@ describe('codex adapter', () => { const files = codexAdapter.fileList(basePath); expect(files.map(f => f.path)).toEqual([ join(basePath, '.codex', 'config.toml'), - join(basePath, '.codex', 'agents', 'recon.toml'), + join(basePath, '.codex', 'agents', 'sentinel.toml'), join(basePath, '.codex', 'agents', 'oracle.toml'), join(basePath, '.codex', 'agents', 'architect.toml'), ]); @@ -81,7 +81,7 @@ describe('codex adapter', () => { codexAdapter.installAgents(s, basePath); codexAdapter.remove(s, basePath); - expect(existsSync(join(basePath, '.codex', 'agents', 'recon.toml'))).toBe(false); + expect(existsSync(join(basePath, '.codex', 'agents', 'sentinel.toml'))).toBe(false); expect(existsSync(join(basePath, '.codex', 'agents', 'oracle.toml'))).toBe(false); expect(existsSync(join(basePath, '.codex', 'agents', 'architect.toml'))).toBe(false); expect(existsSync(join(basePath, '.codex', 'agents'))).toBe(false); @@ -118,7 +118,7 @@ describe('codex adapter', () => { expect(existsSync(userAgent)).toBe(true); expect(existsSync(agentsDir)).toBe(true); - expect(existsSync(join(agentsDir, 'recon.toml'))).toBe(false); + expect(existsSync(join(agentsDir, 'sentinel.toml'))).toBe(false); // Cleanup so the empty-dir removal does not affect other assertions rmSync(agentsDir, { recursive: true, force: true }); diff --git a/src/adapters/codex.ts b/src/adapters/codex.ts index 841ea01..a7a2f81 100644 --- a/src/adapters/codex.ts +++ b/src/adapters/codex.ts @@ -4,7 +4,7 @@ import inquirer from 'inquirer'; import * as TOML from 'smol-toml'; import { readTemplate } from '../utils/templates.js'; import type { CodexConfig, HarnessAdapter, FileEntry, Provider, HarnessSection } from '../types.js'; -import { AGENT_NAMES } from '../constants.js'; +import { AGENT_NAMES, LEGACY_AGENT_NAME } from '../constants.js'; import { FileWriteError, HarnessConfigError } from '../utils/errors.js'; import { shortPath } from '../services/paths.js'; import { SimpleConfigSchema } from '../utils/schemas.js'; @@ -23,7 +23,7 @@ const OPENAI_MODELS = [ const DEFAULT_MODELS = { default_model: 'gpt-5.6-terra', - recon: 'gpt-5.6-luna', + sentinel: 'gpt-5.6-luna', oracle: 'gpt-5.6-sol', architect: 'gpt-5.6-sol', } as const; @@ -83,6 +83,7 @@ function installAgents(section: HarnessSection, basePath: string): void { } catch (err) { throw new FileWriteError(agentsDir, err instanceof Error ? err.message : String(err)); } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.toml`), { force: true }); for (const agent of config.agents) { const template = readTemplate(agent.template); @@ -161,6 +162,7 @@ function remove(section: HarnessSection, basePath: string): void { rmSync(file, { force: true }); } } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.toml`), { force: true }); removeDirIfEmpty(agentsDir); cleanCodexConfig(codexDir); diff --git a/src/adapters/cursor.test.ts b/src/adapters/cursor.test.ts index 6993472..b3b97c7 100644 --- a/src/adapters/cursor.test.ts +++ b/src/adapters/cursor.test.ts @@ -34,9 +34,9 @@ describe('cursor adapter', () => { cursorAdapter.mergeHarnessConfig(cursorAdapter.defaultSection(), basePath); cursorAdapter.installAgents(cursorAdapter.defaultSection(), basePath); - const agentPath = join(basePath, '.cursor', 'agents', 'recon.md'); + const agentPath = join(basePath, '.cursor', 'agents', 'sentinel.md'); expect(readFileSync(agentPath, 'utf-8')).toContain('model: composer-2.5'); - expect(readFileSync(agentPath, 'utf-8')).toContain('name: recon'); + expect(readFileSync(agentPath, 'utf-8')).toContain('name: sentinel'); expect(readFileSync(nativeConfigPath, 'utf-8')).toBe('{"user":"config"}\n'); }); @@ -51,7 +51,7 @@ describe('cursor adapter', () => { writeFileSync(userAgentPath, '---\nname: user\n---\n'); cursorAdapter.remove(section, basePath); - expect(existsSync(join(basePath, '.cursor', 'agents', 'recon.md'))).toBe(false); + expect(existsSync(join(basePath, '.cursor', 'agents', 'sentinel.md'))).toBe(false); expect(existsSync(userAgentPath)).toBe(true); expect(readFileSync(nativeConfigPath, 'utf-8')).toBe('{"user":"config"}\n'); }); diff --git a/src/adapters/cursor.ts b/src/adapters/cursor.ts index eee9928..36cd7e1 100644 --- a/src/adapters/cursor.ts +++ b/src/adapters/cursor.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, readdirSync, rmSync, rmdirSync, writeFileSync } from 'fs'; import { join } from 'path'; import inquirer from 'inquirer'; -import { AGENT_NAMES } from '../constants.js'; +import { AGENT_NAMES, LEGACY_AGENT_NAME } from '../constants.js'; import { shortPath } from '../services/paths.js'; import type { CursorConfig, FileEntry, HarnessAdapter, HarnessSection, Provider } from '../types.js'; import { CursorConfigSchema } from '../utils/schemas.js'; @@ -21,7 +21,7 @@ const CURSOR_MODELS = [ ] as const; const DEFAULT_MODELS = { - recon: 'composer-2.5', + sentinel: 'composer-2.5', oracle: 'claude-opus-4-8-high', architect: 'gpt-5.6-sol-medium', } as const; @@ -76,6 +76,7 @@ function installAgents(section: HarnessSection, basePath: string): void { } catch (err) { throw new FileWriteError(agentsDir, err instanceof Error ? err.message : String(err)); } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); for (const agent of config.agents) { const template = readTemplate(agent.template); @@ -105,6 +106,7 @@ function remove(section: HarnessSection, basePath: string): void { for (const agent of config.agents) { rmSync(join(agentsDir, `${agent.name}.md`), { force: true }); } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); if (existsSync(agentsDir) && readdirSync(agentsDir).length === 0) { rmdirSync(agentsDir); } diff --git a/src/adapters/opencode.test.ts b/src/adapters/opencode.test.ts index 16fc15f..d108f96 100644 --- a/src/adapters/opencode.test.ts +++ b/src/adapters/opencode.test.ts @@ -34,7 +34,7 @@ describe('opencode adapter', () => { expect(openAiSection.build_model).toBe('openai/gpt-5.6-terra'); expect(openAiSection.plan_model).toBe('openai/gpt-5.6-sol'); expect(openAiSection.agents).toEqual([ - { template: 'recon', name: 'recon', model: 'openai/gpt-5.6-luna' }, + { template: 'sentinel', name: 'sentinel', model: 'openai/gpt-5.6-luna' }, { template: 'oracle', name: 'oracle', model: 'openai/gpt-5.6-sol' }, { template: 'architect', name: 'architect', model: 'openai/gpt-5.6-sol' }, ]); @@ -69,7 +69,7 @@ describe('opencode adapter', () => { it('installAgents writes agent files', () => { opencodeAdapter.installAgents(section(), basePath); const agentsDir = join(getOpencodeRoot(basePath), 'agent'); - for (const name of ['recon', 'oracle', 'architect']) { + for (const name of ['sentinel', 'oracle', 'architect']) { const path = join(agentsDir, `${name}.md`); expect(existsSync(path)).toBe(true); const content = readFileSync(path, 'utf-8'); @@ -84,7 +84,7 @@ describe('opencode adapter', () => { const files = opencodeAdapter.fileList(basePath); expect(files.map(f => f.path)).toEqual([ join(basePath, 'opencode.json'), - join(getOpencodeRoot(basePath), 'agent', 'recon.md'), + join(getOpencodeRoot(basePath), 'agent', 'sentinel.md'), join(getOpencodeRoot(basePath), 'agent', 'oracle.md'), join(getOpencodeRoot(basePath), 'agent', 'architect.md'), ]); @@ -103,7 +103,7 @@ describe('opencode adapter', () => { opencodeAdapter.remove(s, basePath); - expect(existsSync(join(root, 'agent', 'recon.md'))).toBe(false); + expect(existsSync(join(root, 'agent', 'sentinel.md'))).toBe(false); expect(existsSync(join(root, 'agent', 'oracle.md'))).toBe(false); expect(existsSync(join(root, 'agent', 'architect.md'))).toBe(false); expect(existsSync(join(root, 'agent'))).toBe(false); @@ -141,7 +141,7 @@ describe('opencode adapter', () => { expect(existsSync(userAgent)).toBe(true); expect(existsSync(agentsDir)).toBe(true); - expect(existsSync(join(agentsDir, 'recon.md'))).toBe(false); + expect(existsSync(join(agentsDir, 'sentinel.md'))).toBe(false); rmSync(agentsDir, { recursive: true, force: true }); }); diff --git a/src/adapters/opencode.ts b/src/adapters/opencode.ts index e0858b3..78756d8 100644 --- a/src/adapters/opencode.ts +++ b/src/adapters/opencode.ts @@ -3,7 +3,7 @@ import { join } from 'path'; import inquirer from 'inquirer'; import { readTemplate } from '../utils/templates.js'; import type { OpenCodeConfig, HarnessAdapter, FileEntry, Provider, OpenCodeProvider, HarnessSection } from '../types.js'; -import { AGENT_NAMES } from '../constants.js'; +import { AGENT_NAMES, LEGACY_AGENT_NAME } from '../constants.js'; import { FileWriteError, HarnessConfigError } from '../utils/errors.js'; import { shortPath, getGlobalOpencodeDir } from '../services/paths.js'; import { OpenCodeConfigSchema } from '../utils/schemas.js'; @@ -68,30 +68,30 @@ const PROVIDER_MODELS: Record = { ], }; -const DEFAULT_MODELS: Record = { +const DEFAULT_MODELS: Record = { 'opencode-zen': { - recon: 'opencode/minimax-m2.7', + sentinel: 'opencode/minimax-m2.7', oracle: 'opencode/kimi-k2.6', architect: 'opencode/deepseek-v4-pro', build: 'opencode/deepseek-v4-flash', plan: 'opencode/deepseek-v4-pro', }, 'opencode-go': { - recon: 'opencode-go/minimax-m2.7', + sentinel: 'opencode-go/minimax-m2.7', oracle: 'opencode-go/kimi-k2.6', architect: 'opencode-go/deepseek-v4-pro', build: 'opencode-go/deepseek-v4-flash', plan: 'opencode-go/deepseek-v4-pro', }, 'amazon-bedrock': { - recon: 'amazon-bedrock/anthropic-claude-haiku-4-5', + sentinel: 'amazon-bedrock/anthropic-claude-haiku-4-5', oracle: 'amazon-bedrock/anthropic-claude-opus-4-7', architect: 'amazon-bedrock/anthropic-claude-opus-4-7', build: 'amazon-bedrock/anthropic-claude-sonnet-4-5', plan: 'amazon-bedrock/anthropic-claude-haiku-4-5', }, openai: { - recon: 'openai/gpt-5.6-luna', + sentinel: 'openai/gpt-5.6-luna', oracle: 'openai/gpt-5.6-sol', architect: 'openai/gpt-5.6-sol', build: 'openai/gpt-5.6-terra', @@ -151,6 +151,7 @@ function installAgents(section: HarnessSection, basePath: string): void { } catch (err) { throw new FileWriteError(agentsDir, err instanceof Error ? err.message : String(err)); } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); for (const agent of config.agents) { const template = readTemplate(agent.template); @@ -175,14 +176,14 @@ function mergeHarnessConfig(section: HarnessSection, basePath: string): void { throw new FileWriteError(basePath, err instanceof Error ? err.message : String(err)); } - const recon = config.agents.find(a => a.name === 'recon'); + const sentinel = config.agents.find(a => a.name === 'sentinel'); const architect = config.agents.find(a => a.name === 'architect'); const atelierFields: Record = { agent: { build: { mode: 'primary', - model: config.build_model || recon?.model || DEFAULT_MODELS[config.provider].build, + model: config.build_model || sentinel?.model || DEFAULT_MODELS[config.provider].build, }, plan: { mode: 'primary', @@ -241,6 +242,7 @@ function removeAgentFiles(opencodeRoot: string, config: OpenCodeConfig): void { rmSync(file, { force: true }); } } + rmSync(join(agentsDir, `${LEGACY_AGENT_NAME}.md`), { force: true }); removeDirIfEmpty(agentsDir); } diff --git a/src/commands/init.test.ts b/src/commands/init.test.ts index 8870f7d..da04b56 100644 --- a/src/commands/init.test.ts +++ b/src/commands/init.test.ts @@ -40,7 +40,7 @@ describe('init', () => { expect(existsSync(join(tempDir, '.atelier/config.json'))).toBe(true); expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(true); - expect(existsSync(join(tempDir, '.claude/agents/recon.md'))).toBe(true); + expect(existsSync(join(tempDir, '.claude/agents/sentinel.md'))).toBe(true); expect(existsSync(join(tempDir, '.claude/agents/oracle.md'))).toBe(true); expect(existsSync(join(tempDir, '.claude/agents/architect.md'))).toBe(true); @@ -59,7 +59,7 @@ describe('init', () => { expect(existsSync(join(tempDir, '.atelier/config.json'))).toBe(true); expect(existsSync(join(opencodeDir, 'opencode.json'))).toBe(true); - expect(existsSync(join(opencodeDir, 'agent/recon.md'))).toBe(true); + expect(existsSync(join(opencodeDir, 'agent/sentinel.md'))).toBe(true); expect(existsSync(join(opencodeDir, 'plugins/atelier.js'))).toBe(false); const config = JSON.parse(readFileSync(join(tempDir, '.atelier/config.json'), 'utf-8')); @@ -72,7 +72,7 @@ describe('init', () => { expect(existsSync(join(tempDir, '.atelier/config.json'))).toBe(true); expect(existsSync(join(tempDir, '.codex/config.toml'))).toBe(true); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(true); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(true); expect(existsSync(join(tempDir, '.codex/agents/oracle.toml'))).toBe(true); expect(existsSync(join(tempDir, '.codex/agents/architect.toml'))).toBe(true); @@ -111,7 +111,7 @@ describe('init', () => { provider: 'opencode-go', build_model: 'opencode-go/deepseek-v4-flash', plan_model: 'opencode-go/deepseek-v4-pro', - recon: 'opencode-go/deepseek-v4-flash', + sentinel: 'opencode-go/deepseek-v4-flash', oracle: 'opencode-go/kimi-k2.6', architect: 'opencode-go/deepseek-v4-pro', confirm: true, @@ -124,7 +124,7 @@ describe('init', () => { const config = JSON.parse(readFileSync(join(tempDir, '.atelier/config.json'), 'utf-8')); expect(config.opencode).toBeDefined(); expect(config.opencode.provider).toBe('opencode-go'); - expect(existsSync(join(opencodeDir, 'agent/recon.md'))).toBe(true); + expect(existsSync(join(opencodeDir, 'agent/sentinel.md'))).toBe(true); }); test('prints the OpenAI authentication command after initialization', async () => { @@ -132,7 +132,7 @@ describe('init', () => { provider: 'openai', build_model: 'openai/gpt-5.6-terra', plan_model: 'openai/gpt-5.6-sol', - recon: 'openai/gpt-5.6-luna', + sentinel: 'openai/gpt-5.6-luna', oracle: 'openai/gpt-5.6-sol', architect: 'openai/gpt-5.6-sol', confirm: true, @@ -166,7 +166,7 @@ describe('init', () => { inquirerAnswers = { harness: 'codex', default_model: 'gpt-5.6-terra', - recon: 'gpt-5.6-luna', + sentinel: 'gpt-5.6-luna', oracle: 'gpt-5.6-sol', architect: 'gpt-5.6-sol', confirm: true, @@ -189,7 +189,7 @@ describe('init', () => { await init({ yes: true, harness: 'cursor' }); expect(readFileSync(nativeConfig, 'utf-8')).toBe('{"model":"user-managed"}\n'); - expect(existsSync(join(tempDir, '.cursor/agents/recon.md'))).toBe(true); + expect(existsSync(join(tempDir, '.cursor/agents/sentinel.md'))).toBe(true); expect(existsSync(join(tempDir, '.cursor/agents/oracle.md'))).toBe(true); expect(existsSync(join(tempDir, '.cursor/agents/architect.md'))).toBe(true); @@ -201,7 +201,7 @@ describe('init', () => { inquirerAnswers = { harness: 'claude', default_model: 'opusplan', - recon: 'haiku', + sentinel: 'haiku', oracle: 'opus', architect: 'opus', confirm: false, @@ -211,7 +211,7 @@ describe('init', () => { expect(existsSync(join(tempDir, '.atelier/config.json'))).toBe(false); expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(false); - expect(existsSync(join(tempDir, '.claude/agents/recon.md'))).toBe(false); + expect(existsSync(join(tempDir, '.claude/agents/sentinel.md'))).toBe(false); }); }); @@ -232,7 +232,7 @@ describe('update', () => { inquirerAnswers = { default_model: 'opusplan', - recon: 'haiku', + sentinel: 'haiku', oracle: 'opus', architect: 'sonnet', confirm: true, @@ -254,7 +254,7 @@ describe('update', () => { provider: 'opencode-zen', build_model: 'opencode/deepseek-v4-flash', plan_model: 'opencode/deepseek-v4-pro', - recon: 'opencode/minimax-m2.7', + sentinel: 'opencode/minimax-m2.7', oracle: 'opencode/kimi-k2.6', architect: 'opencode/deepseek-v4-pro', confirm: true, @@ -278,7 +278,7 @@ describe('remove', () => { await remove({ harness: 'claude' }); - expect(existsSync(join(tempDir, '.claude/agents/recon.md'))).toBe(false); + expect(existsSync(join(tempDir, '.claude/agents/sentinel.md'))).toBe(false); expect(existsSync(join(tempDir, '.claude/agents/oracle.md'))).toBe(false); expect(existsSync(join(tempDir, '.claude/agents/architect.md'))).toBe(false); expect(existsSync(join(tempDir, '.atelier'))).toBe(false); @@ -293,7 +293,7 @@ describe('remove', () => { await remove({ harness: 'opencode' }); - expect(existsSync(join(opencodeDir, 'agent/recon.md'))).toBe(false); + expect(existsSync(join(opencodeDir, 'agent/sentinel.md'))).toBe(false); expect(existsSync(join(opencodeDir, 'plugins/atelier.js'))).toBe(false); expect(existsSync(join(opencodeDir, 'opencode.json'))).toBe(false); expect(existsSync(join(tempDir, '.atelier'))).toBe(false); @@ -306,7 +306,7 @@ describe('remove', () => { await remove({ harness: 'codex' }); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(false); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(false); expect(existsSync(join(tempDir, '.codex/agents/oracle.toml'))).toBe(false); expect(existsSync(join(tempDir, '.codex/agents/architect.toml'))).toBe(false); expect(existsSync(join(tempDir, '.codex/config.toml'))).toBe(false); diff --git a/src/commands/update-remove.test.ts b/src/commands/update-remove.test.ts index 05cd648..687f51f 100644 --- a/src/commands/update-remove.test.ts +++ b/src/commands/update-remove.test.ts @@ -60,24 +60,24 @@ describe('update', () => { expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(true); expect(existsSync(join(tempDir, '.codex/config.toml'))).toBe(true); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(true); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(true); const claudeSettingsBefore = readFileSync(join(tempDir, '.claude/settings.json'), 'utf-8'); // Delete a codex file to prove regeneration - rmSync(join(tempDir, '.codex/agents/recon.toml')); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(false); + rmSync(join(tempDir, '.codex/agents/sentinel.toml')); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(false); inquirerAnswers = { default_model: 'gpt-5.6-terra', - recon: 'gpt-5.6-luna', + sentinel: 'gpt-5.6-luna', oracle: 'gpt-5.6-sol', architect: 'gpt-5.6-sol', confirm: true, }; await update({ harness: 'codex' }); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(true); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(true); expect(existsSync(join(tempDir, '.codex/agents/oracle.toml'))).toBe(true); expect(existsSync(join(tempDir, '.codex/agents/architect.toml'))).toBe(true); expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(true); @@ -93,7 +93,7 @@ describe('update', () => { inquirerAnswers = { default_model: 'gpt-5.6-luna', - recon: 'gpt-5.6-luna', + sentinel: 'gpt-5.6-luna', oracle: 'gpt-5.6-luna', architect: 'gpt-5.6-luna', confirm: false, @@ -108,19 +108,19 @@ describe('update', () => { await init({ yes: true, harness: 'claude' }); await init({ yes: true, harness: 'codex' }); - rmSync(join(tempDir, '.codex/agents/recon.toml')); + rmSync(join(tempDir, '.codex/agents/sentinel.toml')); inquirerAnswers = { harness: 'codex', default_model: 'gpt-5.6-terra', - recon: 'gpt-5.6-luna', + sentinel: 'gpt-5.6-luna', oracle: 'gpt-5.6-sol', architect: 'gpt-5.6-sol', confirm: true, }; await update({}); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(true); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(true); }); test('regenerates Cursor agents and persists selected models', async () => { @@ -128,7 +128,7 @@ describe('update', () => { const nativeConfig = join(tempDir, '.cursor/cli-config.json'); writeFileSync(nativeConfig, '{"model":"user-managed"}\n'); inquirerAnswers = { - recon: 'cursor-grok-4.5-high', + sentinel: 'cursor-grok-4.5-high', oracle: 'kimi-k2.7-code', architect: 'glm-5.2-high', confirm: true, @@ -138,11 +138,11 @@ describe('update', () => { const config = JSON.parse(readFileSync(join(tempDir, '.atelier/config.json'), 'utf-8')); expect(config.cursor.agents).toEqual([ - { template: 'recon', name: 'recon', model: 'cursor-grok-4.5-high' }, + { template: 'sentinel', name: 'sentinel', model: 'cursor-grok-4.5-high' }, { template: 'oracle', name: 'oracle', model: 'kimi-k2.7-code' }, { template: 'architect', name: 'architect', model: 'glm-5.2-high' }, ]); - expect(readFileSync(join(tempDir, '.cursor/agents/recon.md'), 'utf-8')).toContain('model: cursor-grok-4.5-high'); + expect(readFileSync(join(tempDir, '.cursor/agents/sentinel.md'), 'utf-8')).toContain('model: cursor-grok-4.5-high'); expect(readFileSync(nativeConfig, 'utf-8')).toBe('{"model":"user-managed"}\n'); }); }); @@ -171,18 +171,18 @@ describe('remove', () => { await init({ yes: true, harness: 'codex' }); expect(existsSync(join(tempDir, '.claude'))).toBe(true); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(true); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(true); await remove({ harness: 'codex' }); // Codex agent files removed - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(false); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(false); expect(existsSync(join(tempDir, '.codex/agents/oracle.toml'))).toBe(false); expect(existsSync(join(tempDir, '.codex/agents/architect.toml'))).toBe(false); // Claude files untouched expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(true); - expect(existsSync(join(tempDir, '.claude/agents/recon.md'))).toBe(true); + expect(existsSync(join(tempDir, '.claude/agents/sentinel.md'))).toBe(true); // Config: claude section preserved, codex section removed const config = JSON.parse(readFileSync(join(tempDir, '.atelier/config.json'), 'utf-8')); @@ -271,7 +271,7 @@ describe('remove', () => { inquirerAnswers = { harness: 'codex' }; await remove({}); - expect(existsSync(join(tempDir, '.codex/agents/recon.toml'))).toBe(false); + expect(existsSync(join(tempDir, '.codex/agents/sentinel.toml'))).toBe(false); expect(existsSync(join(tempDir, '.claude/settings.json'))).toBe(true); }); @@ -283,7 +283,7 @@ describe('remove', () => { await remove({ harness: 'cursor' }); - expect(existsSync(join(tempDir, '.cursor/agents/recon.md'))).toBe(false); + expect(existsSync(join(tempDir, '.cursor/agents/sentinel.md'))).toBe(false); expect(readFileSync(nativeConfig, 'utf-8')).toBe('{"model":"user-managed"}\n'); expect(existsSync(join(tempDir, '.atelier/config.json'))).toBe(false); }); diff --git a/src/constants.ts b/src/constants.ts index 49c0c7d..a5fc9ad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,3 @@ export const Harness = ['claude', 'opencode', 'codex', 'cursor'] as const; -export const AGENT_NAMES = ['recon', 'oracle', 'architect'] as const; +export const AGENT_NAMES = ['sentinel', 'oracle', 'architect'] as const; +export const LEGACY_AGENT_NAME = 'recon'; diff --git a/src/services/prompt.test.ts b/src/services/prompt.test.ts index 205ff79..be9efaf 100644 --- a/src/services/prompt.test.ts +++ b/src/services/prompt.test.ts @@ -63,13 +63,13 @@ describe('prompt', () => { it('prompts for default model and agent models for simple config', async () => { mockAnswers.push( { default_model: 'sonnet' }, - { recon: 'haiku', oracle: 'opus', architect: 'opus' } + { sentinel: 'haiku', oracle: 'opus', architect: 'opus' } ); const section = await promptForSection(simpleAdapter, { default_model: 'haiku', agents: [ - { template: 'recon', name: 'recon', model: 'haiku' }, + { template: 'sentinel', name: 'sentinel', model: 'haiku' }, { template: 'oracle', name: 'oracle', model: 'opus' }, { template: 'architect', name: 'architect', model: 'opus' }, ], @@ -77,7 +77,7 @@ describe('prompt', () => { expect(section.default_model).toBe('sonnet'); expect(section.agents).toEqual([ - { template: 'recon', name: 'recon', model: 'haiku' }, + { template: 'sentinel', name: 'sentinel', model: 'haiku' }, { template: 'oracle', name: 'oracle', model: 'opus' }, { template: 'architect', name: 'architect', model: 'opus' }, ]); @@ -87,7 +87,7 @@ describe('prompt', () => { mockAnswers.push( { provider: 'opencode-go' }, { build_model: 'b', plan_model: 'c' }, - { recon: 'a', oracle: 'b', architect: 'c' } + { sentinel: 'a', oracle: 'b', architect: 'c' } ); const section = await promptForSection(openCodeAdapter, { @@ -95,7 +95,7 @@ describe('prompt', () => { build_model: 'a', plan_model: 'b', agents: [ - { template: 'recon', name: 'recon', model: 'a' }, + { template: 'sentinel', name: 'sentinel', model: 'a' }, { template: 'oracle', name: 'oracle', model: 'b' }, { template: 'architect', name: 'architect', model: 'c' }, ], @@ -105,7 +105,7 @@ describe('prompt', () => { expect(section.build_model).toBe('b'); expect(section.plan_model).toBe('c'); expect(section.agents).toEqual([ - { template: 'recon', name: 'recon', model: 'a' }, + { template: 'sentinel', name: 'sentinel', model: 'a' }, { template: 'oracle', name: 'oracle', model: 'b' }, { template: 'architect', name: 'architect', model: 'c' }, ]); @@ -123,8 +123,8 @@ describe('prompt', () => { it('formatFileList renders exists marker', () => { const files = [ { path: '~/.claude/settings.json', exists: true }, - { path: '~/.claude/agents/recon.md', exists: false }, + { path: '~/.claude/agents/sentinel.md', exists: false }, ]; - expect(formatFileList(files)).toBe(' ~ ~/.claude/settings.json\n + ~/.claude/agents/recon.md'); + expect(formatFileList(files)).toBe(' ~ ~/.claude/settings.json\n + ~/.claude/agents/sentinel.md'); }); }); diff --git a/src/utils/config.test.ts b/src/utils/config.test.ts index a73cfa2..fbae713 100644 --- a/src/utils/config.test.ts +++ b/src/utils/config.test.ts @@ -27,7 +27,7 @@ describe('config', () => { claude: { default_model: 'opus', agents: [ - { template: 'recon', name: 'recon', model: 'haiku' }, + { template: 'sentinel', name: 'sentinel', model: 'haiku' }, { template: 'oracle', name: 'oracle', model: 'opus' }, { template: 'architect', name: 'architect', model: 'sonnet' }, ], @@ -137,6 +137,30 @@ describe('config', () => { } }); + test('readConfig migrates legacy recon entries without changing models', async () => { + const { writeFileSync, mkdirSync } = await import('fs'); + const { readConfig } = await import('./config.js'); + const configPath = join(tempDir, '.atelier/config.json'); + mkdirSync(join(tempDir, '.atelier'), { recursive: true }); + writeFileSync(configPath, JSON.stringify({ + version: '1.0.0', + skills_source: 'martinffx/atelier', + skills_path: '~/.agents/skills', + claude: { + default_model: 'opus', + agents: [{ template: 'recon', name: 'recon', model: 'custom-model' }], + }, + })); + + const result = readConfig(configPath); + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.claude?.agents).toEqual([ + { template: 'sentinel', name: 'sentinel', model: 'custom-model' }, + ]); + } + }); + test('getDefaultConfig returns valid config with default models for claude', async () => { const { getDefaultConfig } = await import('./config.js'); @@ -148,8 +172,8 @@ describe('config', () => { expect(config.claude?.default_model).toBe('opusplan'); expect(config.claude?.agents).toHaveLength(3); - const recon = config.claude?.agents.find(a => a.name === 'recon'); - expect(recon?.model).toBe('haiku'); + const sentinel = config.claude?.agents.find(a => a.name === 'sentinel'); + expect(sentinel?.model).toBe('haiku'); const oracle = config.claude?.agents.find(a => a.name === 'oracle'); expect(oracle?.model).toBe('opus'); @@ -167,8 +191,8 @@ describe('config', () => { expect(config.codex?.default_model).toBe('gpt-5.6-terra'); expect(config.codex?.agents).toHaveLength(3); - const recon = config.codex?.agents.find(a => a.name === 'recon'); - expect(recon?.model).toBe('gpt-5.6-luna'); + const sentinel = config.codex?.agents.find(a => a.name === 'sentinel'); + expect(sentinel?.model).toBe('gpt-5.6-luna'); const oracle = config.codex?.agents.find(a => a.name === 'oracle'); expect(oracle?.model).toBe('gpt-5.6-sol'); @@ -182,8 +206,8 @@ describe('config', () => { expect(section.provider).toBe('opencode-zen'); - const recon = section.agents.find(a => a.name === 'recon'); - expect(recon?.model).toBe('opencode/minimax-m2.7'); + const sentinel = section.agents.find(a => a.name === 'sentinel'); + expect(sentinel?.model).toBe('opencode/minimax-m2.7'); const oracle = section.agents.find(a => a.name === 'oracle'); expect(oracle?.model).toBe('opencode/kimi-k2.6'); @@ -197,8 +221,8 @@ describe('config', () => { expect(section.provider).toBe('opencode-go'); - const recon = section.agents.find(a => a.name === 'recon'); - expect(recon?.model).toBe('opencode-go/minimax-m2.7'); + const sentinel = section.agents.find(a => a.name === 'sentinel'); + expect(sentinel?.model).toBe('opencode-go/minimax-m2.7'); const oracle = section.agents.find(a => a.name === 'oracle'); expect(oracle?.model).toBe('opencode-go/kimi-k2.6'); @@ -224,8 +248,8 @@ describe('config', () => { expect(section.provider).toBe('opencode-zen'); - const recon = section.agents.find(a => a.name === 'recon'); - expect(recon?.model).toBe('opencode/minimax-m2.7'); + const sentinel = section.agents.find(a => a.name === 'sentinel'); + expect(sentinel?.model).toBe('opencode/minimax-m2.7'); }); test('cursor config accepts only agent selections', async () => { @@ -257,7 +281,7 @@ describe('config', () => { claude: { default_model: 'opus', agents: [ - { template: 'recon', name: 'recon', model: 'haiku' }, + { template: 'sentinel', name: 'sentinel', model: 'haiku' }, { template: 'oracle', name: 'oracle', model: 'opus' }, { template: 'architect', name: 'architect', model: 'sonnet' }, ], @@ -265,14 +289,14 @@ describe('config', () => { codex: { default_model: 'gpt-5.6-terra', agents: [ - { template: 'recon', name: 'recon', model: 'gpt-5.6-luna' }, + { template: 'sentinel', name: 'sentinel', model: 'gpt-5.6-luna' }, { template: 'oracle', name: 'oracle', model: 'gpt-5.6-sol' }, { template: 'architect', name: 'architect', model: 'gpt-5.6-sol' }, ], }, cursor: { agents: [ - { template: 'recon', name: 'recon', model: 'composer-2.5' }, + { template: 'sentinel', name: 'sentinel', model: 'composer-2.5' }, { template: 'oracle', name: 'oracle', model: 'claude-opus-4-8-high' }, { template: 'architect', name: 'architect', model: 'gpt-5.6-sol-medium' }, ], @@ -302,7 +326,7 @@ describe('config', () => { skills_path: '~/.agents/skills', claude: { default_model: 'opus', - agents: [{ template: 'recon', name: 'recon', model: 'haiku' }], + agents: [{ template: 'sentinel', name: 'sentinel', model: 'haiku' }], }, }; diff --git a/src/utils/config.ts b/src/utils/config.ts index 9829ec5..1dd67e7 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -3,7 +3,7 @@ import { dirname, join } from 'path'; import { homedir } from 'os'; import { z } from 'zod'; import type { AtelierConfig, Harness, SharedConfig, HarnessSection } from '../types.js'; -import { Harness as Harnesses } from '../constants.js'; +import { Harness as Harnesses, LEGACY_AGENT_NAME } from '../constants.js'; import { listAdapters, getAdapter } from '../registry.js'; import { InvalidConfigError } from './errors.js'; import type { Result } from './result.js'; @@ -73,13 +73,44 @@ export function readConfig(path: string = CONFIG_PATH): Result) }; + for (const harness of Harnesses) { + const section = migrated[harness]; + if (typeof section !== 'object' || section === null || !Array.isArray((section as Record).agents)) { + continue; + } + + const agents = (section as Record).agents as unknown[]; + migrated[harness] = { + ...(section as Record), + agents: agents.map(agent => { + if (typeof agent !== 'object' || agent === null) { + return agent; + } + const entry = agent as Record; + return { + ...entry, + template: entry.template === LEGACY_AGENT_NAME ? 'sentinel' : entry.template, + name: entry.name === LEGACY_AGENT_NAME ? 'sentinel' : entry.name, + }; + }), + }; + } + return migrated; +} + function isOldFlatConfig(config: unknown): boolean { if (typeof config !== 'object' || config === null) { return false; diff --git a/src/utils/schemas.test.ts b/src/utils/schemas.test.ts index 0309876..6e5a8b1 100644 --- a/src/utils/schemas.test.ts +++ b/src/utils/schemas.test.ts @@ -5,7 +5,7 @@ describe('CursorConfigSchema', () => { it('accepts only configured Cursor agents', () => { const config = { agents: [ - { template: 'recon', name: 'recon', model: 'composer-2.5' }, + { template: 'sentinel', name: 'sentinel', model: 'composer-2.5' }, { template: 'oracle', name: 'oracle', model: 'claude-opus-4-8-high' }, { template: 'architect', name: 'architect', model: 'gpt-5.6-sol-medium' }, ],