Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/multi-tool-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@taskless/cli": patch
---

Add multi-tool detection for `taskless init`. The CLI now detects and installs skills for OpenCode (`.opencode/`, `opencode.jsonc`, `opencode.json`), Cursor (`.cursor/`, `.cursorrules`), and Claude Code (now also via `CLAUDE.md`). When no tools are detected, skills are installed to `.agents/skills/` as a fallback.
30 changes: 30 additions & 0 deletions .conventions/STYLEGUIDE-CODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,36 @@ interface GitHubComment {
- You need a subset/transformation of the original type for your domain model
- You're defining types for your own APIs (not external ones)

### Export Types Referenced by Public API Signatures

**DO NOT** remove `export` from types that are transitively referenced by exported functions, values, or other exported types — even if tools like knip report them as "unused exports." With `declaration: true` in `tsconfig`, TypeScript requires all types in exported signatures to be exported themselves.

Before removing an `export` from a type, check whether any exported function or value references it in its signature (parameters, return types, or fields of other exported types).

```typescript
// ✅ Good - Type is exported because it's used in an exported function's return type
export interface VerifyResult {
success: boolean;
schema: LayerResult;
}

export interface LayerResult {
valid: boolean;
errors: string[];
}

export async function verifyRule(path: string): Promise<VerifyResult> { ... }

// ❌ Bad - Knip says LayerResult is "unused" so you remove the export
interface LayerResult { ... } // breaks declaration emit for VerifyResult
```

**Rationale:**

- Knip tracks direct import usage, not transitive type reachability through exported signatures
- Removing these exports causes `declaration: true` to fail with "exported function has or is using private name" errors
- The fix is tedious — each type must be re-exported individually, often across multiple review cycles

## Cross-Worker Durable Object Access

### Use DurableObjectRPC for Cross-Worker DO Calls
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-08
61 changes: 61 additions & 0 deletions openspec/changes/archive/2026-04-07-multi-tool-detection/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## Context

The CLI's `detectTools()` function checks for known directories to decide where to install skills. Currently, `ToolDescriptor` uses a single `dir` field for both detection and install path. Only Claude Code (`.claude/` directory) is supported.

We need to support four tool targets with detection signals that include both files and directories, and add a fallback install path when no tools are detected.

## Goals / Non-Goals

**Goals:**

- Detect Claude Code, OpenCode, Cursor, and install skills into each detected tool's directory
- Support file-based detection signals (e.g., `CLAUDE.md`, `.cursorrules`, `opencode.json`)
- Provide `.agents/skills/` as a fallback when no tools are detected
- Keep the install logic unchanged — only detection and the tool registry expand

**Non-Goals:**

- Adapting skill content per tool (all tools get identical `SKILL.md` files)
- Supporting tool-specific config file generation (e.g., creating `.cursorrules`)
- Auto-detecting tools that require network or process inspection
- Adding commands support for any tool other than Claude Code

## Decisions

### Split detection signals from install root

**Decision:** Replace the single `dir` field in `ToolDescriptor` with separate `detect` and `installDir` fields.

**Rationale:** Detection of `CLAUDE.md` (a file) should trigger installation into `.claude/` (a directory). The current design conflates these — `dir` is both the detection check and the install root. Splitting them keeps each concern clean.

**`detect`** is an array of signal objects:

```typescript
type DetectionSignal =
| { type: "directory"; path: string }
| { type: "file"; path: string };
```

**`installDir`** is the directory root for skill (and optionally command) installation. This replaces `dir`.

**Alternative considered:** Keep `dir` and add an optional `additionalSignals` array. Rejected because it makes `dir` semantically overloaded — it would mean "primary detection signal AND install root," which is confusing when the primary signal for a tool might be a file.

### Fallback is handled in init, not in detection

**Decision:** The `.agents/` fallback is not a detected tool. Instead, `init.ts` checks whether any installs were made after the tool loop and falls back to a hardcoded `AGENTS_FALLBACK` descriptor.

**Rationale:** `.agents/` has no detection signal — it exists precisely because nothing was detected. Modeling it as a detected tool would require a sentinel like `detect: []` meaning "always match," which is misleading. Keeping it as explicit fallback logic in `init.ts` makes the intent clear.

### Detection uses `stat` for both directories and files

**Decision:** Directory signals use `stat().isDirectory()` (existing pattern). File signals use `stat().isFile()` so detection only succeeds for actual files, not directories that happen to exist at the same path.

**Rationale:** Using `stat` consistently for both signal types keeps detection semantics explicit and avoids false positives for file-based signals. For directories we need `isDirectory()`, and for files we need `isFile()` to distinguish them from directories of the same name.

## Risks / Trade-offs

**[Multiple installs create duplication]** → A repo with both `.claude/` and `.cursor/` gets skills in two places. This is intentional — each tool reads from its own directory. Users who find this noisy can remove the tool directory they don't use.

**[File detection may create directories]** → Detecting `CLAUDE.md` without a `.claude/` directory means `mkdir -p .claude/skills/` runs during install, creating a new directory. This is acceptable — if the user has `CLAUDE.md`, they're using Claude Code and will benefit from having skills installed. Same applies to `.cursorrules` creating `.cursor/skills/`.

**[`.cursorrules` false positives]** → `.cursorrules` is widely used and still actively supported by Cursor. Detection of it is a valid signal that the project uses Cursor.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Why

The CLI currently only detects Claude Code (via `.claude/` directory). As AI coding tools proliferate — OpenCode, Cursor, and generic agent frameworks — users expect `taskless init` to install skills wherever their tools are configured. Expanding detection also provides a fallback (`.agents/skills/`) so that projects without a recognized tool still get skills installed.

## What Changes

- **Expand Claude Code detection** to also trigger on a `CLAUDE.md` file in the project root (not just the `.claude/` directory).
- **Add OpenCode detection** via `.opencode/` directory, `opencode.jsonc`, or `opencode.json` in the project root. Skills install to `.opencode/skills/<name>/SKILL.md`.
- **Add Cursor detection** via `.cursor/` directory or `.cursorrules` file in the project root. Skills install to `.cursor/skills/<name>/SKILL.md`.
- **Add `.agents/` fallback** — if no recognized tools were detected (zero installs made), install skills to `.agents/skills/<name>/SKILL.md`.
- **Split detection signals from install paths** in `ToolDescriptor` — detection can now be triggered by files or directories, while the install root is a separate field.
- Commands remain Claude Code-only; all other tools receive skills only.

## Capabilities

### New Capabilities

None.

### Modified Capabilities

- `cli-init`: The init command's detection and install flow changes to support multiple tools, file-based detection signals, and the `.agents/` fallback.

## Impact

- **Code**: `packages/cli/src/install/install.ts` — `ToolDescriptor` type, `TOOLS` registry, `detectTools()` function. `packages/cli/src/commands/init.ts` — fallback logic after install loop.
- **Tests**: Existing detection tests need updating; new tests for each tool's detection signals and the fallback behavior.
- **User-facing**: `taskless init` output changes to list multiple tools when detected; fallback message changes from "no tools found" to installing into `.agents/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
## ADDED Requirements

### Requirement: Tool descriptor separates detection from install path

Each tool in the registry SHALL have a `detect` array of signals and a separate `installDir` string. Detection signals SHALL be objects with a `type` field (`"directory"` or `"file"`) and a `path` field relative to the project root.

#### Scenario: Tool detected by directory signal

- **WHEN** a tool descriptor has `{ type: "directory", path: ".claude" }` in its `detect` array
- **AND** `.claude/` exists as a directory in the project root
- **THEN** the tool SHALL be detected

#### Scenario: Tool detected by file signal

- **WHEN** a tool descriptor has `{ type: "file", path: "CLAUDE.md" }` in its `detect` array
- **AND** `CLAUDE.md` exists as a file in the project root
- **THEN** the tool SHALL be detected

#### Scenario: Tool detected by any matching signal

- **WHEN** a tool descriptor has multiple signals in its `detect` array
- **AND** at least one signal matches a file or directory in the project root
- **THEN** the tool SHALL be detected

#### Scenario: Tool not detected when no signals match

- **WHEN** no signals in a tool's `detect` array match files or directories in the project root
- **THEN** the tool SHALL NOT be detected

### Requirement: Claude Code detection signals

Claude Code SHALL be detected when any of the following exist in the project root:

- `.claude/` directory
- `CLAUDE.md` file

Skills SHALL be installed to `.claude/skills/<name>/SKILL.md`. Commands SHALL be installed to `.claude/commands/tskl/`.

#### Scenario: Claude Code detected by .claude directory

- **WHEN** `.claude/` exists as a directory in the project root
- **THEN** Claude Code SHALL be detected
- **AND** skills SHALL be installed to `.claude/skills/`

#### Scenario: Claude Code detected by CLAUDE.md file

- **WHEN** `CLAUDE.md` exists as a file in the project root
- **AND** `.claude/` directory does not exist
- **THEN** Claude Code SHALL be detected
- **AND** skills SHALL be installed to `.claude/skills/`

### Requirement: OpenCode detection signals

OpenCode SHALL be detected when any of the following exist in the project root:

- `.opencode/` directory
- `opencode.jsonc` file
- `opencode.json` file

Skills SHALL be installed to `.opencode/skills/<name>/SKILL.md`. OpenCode SHALL NOT receive commands.

#### Scenario: OpenCode detected by .opencode directory

- **WHEN** `.opencode/` exists as a directory in the project root
- **THEN** OpenCode SHALL be detected

#### Scenario: OpenCode detected by opencode.jsonc file

- **WHEN** `opencode.jsonc` exists as a file in the project root
- **THEN** OpenCode SHALL be detected

#### Scenario: OpenCode detected by opencode.json file

- **WHEN** `opencode.json` exists as a file in the project root
- **THEN** OpenCode SHALL be detected

### Requirement: Cursor detection signals

Cursor SHALL be detected when any of the following exist in the project root:

- `.cursor/` directory
- `.cursorrules` file

Skills SHALL be installed to `.cursor/skills/<name>/SKILL.md`. Cursor SHALL NOT receive commands.

#### Scenario: Cursor detected by .cursor directory

- **WHEN** `.cursor/` exists as a directory in the project root
- **THEN** Cursor SHALL be detected

#### Scenario: Cursor detected by .cursorrules file

- **WHEN** `.cursorrules` exists as a file in the project root
- **THEN** Cursor SHALL be detected

### Requirement: Agents fallback install

When `taskless init` completes with zero tool installs (no tools were detected), skills SHALL be installed to `.agents/skills/<name>/SKILL.md`. The `.agents/` target SHALL NOT receive commands. The `.agents/` target SHALL NOT be part of tool detection — it is used only as a fallback.

#### Scenario: Fallback installs to .agents when no tools detected

- **WHEN** a user runs `taskless init`
- **AND** no tools are detected in the project root
- **THEN** skills SHALL be installed to `.agents/skills/`

#### Scenario: Fallback not used when tools are detected

- **WHEN** a user runs `taskless init`
- **AND** at least one tool is detected
- **THEN** skills SHALL NOT be installed to `.agents/skills/`

#### Scenario: Fallback does not install commands

- **WHEN** the `.agents/` fallback is used
- **THEN** no command files SHALL be written

### Requirement: Detection checks files and directories with stat

Directory detection signals SHALL use `fs.stat()` and verify `isDirectory()`. File detection signals SHALL use `fs.stat()` and verify `isFile()` to avoid false positives on directories. All detection checks SHALL run in parallel.

#### Scenario: Directory signal uses stat

- **WHEN** a directory detection signal is evaluated
- **THEN** `fs.stat()` SHALL be called on the path
- **AND** `isDirectory()` SHALL return true for detection to succeed

#### Scenario: File signal uses stat

- **WHEN** a file detection signal is evaluated
- **THEN** `fs.stat()` SHALL be called on the path
- **AND** `isFile()` SHALL return true for detection to succeed

#### Scenario: Detection runs in parallel

- **WHEN** `detectTools()` is called
- **THEN** all tool detection checks SHALL run concurrently via `Promise.all`

## MODIFIED Requirements

### Requirement: Tool detection via filesystem inspection

The CLI SHALL detect installed AI tools by checking for known detection signals (files and directories) via parallel filesystem checks. Detection SHALL NOT rely on a config file. The tool registry SHALL be a typed array of tool descriptors maintained in `packages/cli/src/install/install.ts`. Each tool descriptor SHALL have a `detect` array of signals and a separate `installDir` for the install root.

#### Scenario: Claude Code is detected

- **WHEN** a `.claude/` directory or `CLAUDE.md` file exists in the working directory
- **THEN** the CLI SHALL detect Claude Code as an installed tool

#### Scenario: OpenCode is detected

- **WHEN** a `.opencode/` directory, `opencode.jsonc` file, or `opencode.json` file exists in the working directory
- **THEN** the CLI SHALL detect OpenCode as an installed tool

#### Scenario: Cursor is detected

- **WHEN** a `.cursor/` directory or `.cursorrules` file exists in the working directory
- **THEN** the CLI SHALL detect Cursor as an installed tool

#### Scenario: Multiple tools are detected

- **WHEN** multiple known tool signals exist (e.g., `.claude/` and `.cursor/`)
- **THEN** the CLI SHALL detect all of them and install skills for each

#### Scenario: No tools detected triggers fallback

- **WHEN** no known tool signals exist in the working directory
- **THEN** the CLI SHALL install skills to `.agents/skills/` as a fallback

### Requirement: Init subcommand installs skills into a repository

The CLI SHALL support a `taskless init` subcommand that installs Taskless skills into the current working directory. The subcommand SHALL also be available as `taskless update` (alias with identical behavior). When no tool directories are detected, the CLI SHALL install skills to `.agents/skills/<name>/SKILL.md` as a fallback.

#### Scenario: Running taskless init installs skills

- **WHEN** a user runs `taskless init` in a repository with at least one detected AI tool
- **THEN** the CLI SHALL write skill files into each detected tool's directory
- **AND** report which tools were updated and how many skills were installed

#### Scenario: Running taskless update behaves identically to init

- **WHEN** a user runs `taskless update`
- **THEN** the behavior SHALL be identical to `taskless init`

#### Scenario: Running init with no detected tools uses fallback

- **WHEN** a user runs `taskless init` in a repository with no detected AI tool signals
- **THEN** the CLI SHALL install skills to `.agents/skills/`
- **AND** report that the fallback location was used
48 changes: 48 additions & 0 deletions openspec/changes/archive/2026-04-07-multi-tool-detection/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## 1. Refactor ToolDescriptor to split detection from install path

Purely structural refactor — no new tools, no behavior change. Claude Code remains the only tool, still detected by `.claude/` directory only.

- [x] 1.1 Add `DetectionSignal` type (`{ type: "directory" | "file"; path: string }`) to `packages/cli/src/install/install.ts`
- [x] 1.2 Replace `dir` field in `ToolDescriptor` with `detect: DetectionSignal[]` and `installDir: string`
- [x] 1.3 Update Claude Code entry in `TOOLS` to use `detect: [{ type: "directory", path: ".claude" }]` and `installDir: ".claude"`
- [x] 1.4 Update `detectTools()` to iterate each tool's `detect` array, using `stat().isDirectory()` for directory signals and `stat().isFile()` for file signals
- [x] 1.5 Update `installForTool`, `removeOwnedSkills`, `removeOwnedCommands`, and `checkStaleness` to use `installDir` instead of `dir`
- [x] 1.6 Create `test/install.test.ts` with baseline detection tests: Claude Code detected via `.claude/` directory, not detected when absent
- [x] 1.7 Test `installForTool` writes skills to `installDir`-based path and creates directories if missing
- [x] 1.8 Test `checkStaleness` reports status using `installDir`-based paths
- [x] 1.9 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm build`

## 2. Expand Claude Code detection and add OpenCode and Cursor tools

Add new detection signals and tool entries. Each detected tool gets skills installed.

- [x] 2.1 Add `CLAUDE.md` file signal to Claude Code's `detect` array
- [x] 2.2 Add OpenCode entry: detect `[".opencode/" dir, "opencode.jsonc" file, "opencode.json" file]`, installDir `.opencode`, no commands
- [x] 2.3 Add Cursor entry: detect `[".cursor/" dir, ".cursorrules" file]`, installDir `.cursor`, no commands
- [x] 2.4 Test `detectTools()` detects Claude Code via `.claude/` directory
- [x] 2.5 Test `detectTools()` detects Claude Code via `CLAUDE.md` file
- [x] 2.6 Test `detectTools()` detects OpenCode via `.opencode/` directory, `opencode.jsonc`, and `opencode.json`
- [x] 2.7 Test `detectTools()` detects Cursor via `.cursor/` directory and `.cursorrules`
- [x] 2.8 Test `detectTools()` returns multiple tools when multiple signals exist
- [x] 2.9 Test `detectTools()` returns empty when no signals match
- [x] 2.10 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm build`

## 3. Add .agents fallback when no tools detected

Replace the "no tools found" message with a fallback install to `.agents/skills/`.

- [x] 3.1 Define `AGENTS_FALLBACK` descriptor: no detect array, installDir `.agents`, no commands
- [x] 3.2 In `init.ts`, track whether any installs were made after the tool loop
- [x] 3.3 If zero installs, call `installForTool(cwd, AGENTS_FALLBACK, skills, [])` and report fallback usage
- [x] 3.4 Remove the old "no supported tool directories detected" message
- [x] 3.5 Test fallback installs to `.agents/skills/` when no tools detected
- [x] 3.6 Test fallback is NOT used when at least one tool is detected
- [x] 3.7 Test fallback does not install commands
- [x] 3.8 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm build`

## 4. Update info command and help text

- [x] 4.1 Verify `info.ts` works with updated `checkStaleness()` (uses `installDir`)
- [x] 4.2 Update `src/help/init.txt` if it references detection behavior
- [x] 4.3 Test `checkStaleness` returns correct status for each newly supported tool (OpenCode, Cursor, `.agents/`)
- [x] 4.4 Run `pnpm typecheck && pnpm lint && pnpm test && pnpm build`
Loading
Loading