diff --git a/docs/iloom-commands.md b/docs/iloom-commands.md index c57ca67a..bdfc4b01 100644 --- a/docs/iloom-commands.md +++ b/docs/iloom-commands.md @@ -34,6 +34,7 @@ Complete documentation for all iloom CLI commands, options, and flags. - [il update](#il-update) - [il feedback](#il-feedback) - [il contribute](#il-contribute) + - [il openclaw](#il-openclaw) - [il telemetry](#il-telemetry) --- @@ -2056,6 +2057,65 @@ il contribute "facebook/react" --- +### il openclaw + +Register iloom as an OpenClaw skill by creating a symlink from the OpenClaw workspace to the project's `openclaw-skill/` directory. + +**Usage:** +```bash +il openclaw [options] +``` + +**Options:** + +| Flag | Description | Default | +|------|-------------|---------| +| `-f, --force` | Overwrite existing skill registration | `false` | +| `-w, --workspace ` | OpenClaw workspace name | `workspace` | + +**Behavior:** + +1. Verifies `openclaw-skill/` directory exists in the project root +2. Checks that OpenClaw is installed (`~/.openclaw` exists) +3. Checks that the specified workspace exists under `~/.openclaw/` +4. Creates `~/.openclaw//skills/` directory if needed +5. Creates symlink: `~/.openclaw//skills/iloom` β†’ `/openclaw-skill/` + +**Examples:** + +```bash +# Register iloom skill in the default workspace +il openclaw + +# Register in a specific workspace +il openclaw --workspace my-workspace +il openclaw -w my-workspace + +# Force overwrite an existing registration +il openclaw --force +il openclaw -f + +# Combine options +il openclaw -f -w my-workspace +``` + +**Error Handling:** + +| Scenario | Behavior | +|----------|----------| +| `openclaw-skill/` missing from project | Error with clear message | +| `~/.openclaw` not found | Friendly error suggesting OpenClaw is not installed | +| Workspace not found | Error suggesting `--workspace` flag | +| Target already correctly linked | Reports "Already linked" and exits successfully | +| Target exists (wrong symlink/file/dir) | Error suggesting `--force` (and `--workspace` if using default workspace) | + +**Notes:** +- This command does not create `~/.openclaw` or workspace directories β€” those are managed by OpenClaw itself +- The `skills/` subdirectory under the workspace is safe to auto-create +- Running the command multiple times is idempotent when the symlink is already correct + +--- + ### il telemetry Manage anonymous usage telemetry settings. diff --git a/openclaw-skill/SKILL.md b/openclaw-skill/SKILL.md new file mode 100644 index 00000000..4709a024 --- /dev/null +++ b/openclaw-skill/SKILL.md @@ -0,0 +1,78 @@ +--- +name: iloom +description: Manage isolated Git worktrees and AI-assisted development workflows with iloom CLI. Use when you need to create workspaces for issues/PRs, commit and merge code, run dev servers, plan and decompose features into issues, enhance issue descriptions with AI, list active workspaces, or configure iloom projects. Covers the full loom lifecycle (init, start, finish, cleanup) and all development commands (spin, commit, rebase, build, test, lint). Also use when the user has an idea for improvement or new feature β€” route through `il plan` for ideation and decomposition. +metadata: { "openclaw": { "emoji": "🧡", "requires": { "anyBins": ["il", "iloom"] } } } +--- + +# iloom + +Manage isolated Git worktrees with AI-assisted development workflows. + +## Terminology + +`` β€” the identifier for a work item on your configured issue tracker. Examples: `42` or `#42` (GitHub), `ENG-456` (Linear), `PROJ-789` (Jira). + +## Execution Modes + +| Mode | Commands | Notes | +|------|----------|-------| +| **Plain exec** (no PTY, no background) | `list`, `issues`, `projects`, `recap`, `--version`, `start --no-claude --no-code --no-dev-server --no-terminal`, `cleanup --force`, `build`, `test`, `lint`, `compile`, `add-issue` | Fast, clean JSON output | +| **Background** (`background:true`) | `plan`, `spin`, `start` (with Claude), `summary`, `enhance`, `commit`, `finish`, `rebase` | Long-running or spawns Claude β€” monitor with `process action:poll sessionId:XXX` | +| **Foreground PTY only** | `init`, `shell` | Interactive β€” not for AI agents | + +See `{baseDir}/references/non-interactive-patterns.md` for the complete execution mode guide, session lifecycle, decision bypass map, and recommended autonomous flag combinations. + +## Project Initialization (First-Time Setup) + +Before using any iloom commands, the project must have a `.iloom/settings.json` file. + +```bash +mkdir -p .iloom +echo '{"mainBranch": "main"}' > .iloom/settings.json +``` + +See `{baseDir}/references/initialization.md` for the complete settings schema, all configuration options, remote configuration for fork workflows, and example configurations. + +## Workflow: Choosing the Right Approach + +### Sizeable Changes (multiple issues, architectural work) + +Use the **plan β†’ review β†’ start β†’ spin β†’ finish** workflow: + +1. **Plan:** `il plan --yolo --print --json-stream 'Description'` (background) β€” decomposes work into issues +2. **Review:** Present the created epic to the user; wait for approval before continuing +3. **Start:** `il start --yolo --no-code --no-dev-server --no-claude --no-terminal --json` (plain exec) β€” creates workspace without Claude +4. **Spin:** `il spin --yolo --print --json-stream` (background) β€” launches Claude separately +5. **Finish:** `il finish --force --cleanup --no-browser --json-stream` (background) β€” merges and cleans up + +### Small Changes (single issue, quick fix) + +Create issue + workspace + launch Claude in one step: + +```bash +bash background:true command:"il start 'Add dark mode support to the settings page' --yolo --no-code --json" +``` + +See `{baseDir}/references/core-workflow.md` for full command flags/examples and `{baseDir}/references/planning-and-issues.md` for planning details. + +## References + +- **Project initialization and settings schema:** See `{baseDir}/references/initialization.md` +- **Core lifecycle commands (init, start, finish, cleanup, list):** See `{baseDir}/references/core-workflow.md` +- **Development commands (spin, commit, rebase, build, test, etc.):** See `{baseDir}/references/development-commands.md` +- **Planning and issue management (plan, add-issue, enhance, issues):** See `{baseDir}/references/planning-and-issues.md` +- **Settings, env vars, and global flags:** See `{baseDir}/references/configuration.md` +- **Non-interactive patterns (PTY, background, autonomous operation):** See `{baseDir}/references/non-interactive-patterns.md` + +## Safety Rules + +1. **Use the right execution mode** for each command β€” see the Execution Modes table above and `{baseDir}/references/non-interactive-patterns.md` for details. +2. **Use `background:true`** for commands that launch Claude or run extended operations: `start` (with Claude), `spin`, `plan`, `commit`, `finish`, `rebase`. +3. **Never run `il finish` without `--force`** in autonomous mode β€” it will hang on confirmation prompts. +4. **Always pass explicit flags** to avoid interactive prompts. See `{baseDir}/references/non-interactive-patterns.md` for the complete decision bypass map. +5. **Use `--json`** when you need to parse command output programmatically. **`--json` and `--json-stream` are mutually exclusive** β€” prefer `--json-stream` for commands that support it (commit, finish, rebase) since it provides incremental visibility. Use `--json` only for commands without `--json-stream` support (list, cleanup, start, etc.). +6. **Prefer manual initialization** over `il init` β€” create `.iloom/settings.json` directly. See `{baseDir}/references/initialization.md`. +7. **Respect worktree isolation** β€” each loom is an independent workspace. Run commands from within the correct worktree directory. +8. **NEVER kill a background session you did not start.** Other looms may be running from separate planning or development sessions (the user's own work, other agents, or prior conversations). When you see unfamiliar background sessions, **leave them alone**. Only kill sessions you explicitly launched in the current workflow. If unsure, ask the user. +9. **Send progress updates mid-turn.** Long-running loom operations (plan, spin, commit, finish) can take minutes. Use the `message` tool to send incremental status updates to the user while waiting β€” don't go silent. Examples: "🧡 Spin started for issue #5, monitoring…", "βœ… Tests passing, spin entering code review phase", "πŸ”€ Merging to main…". Keep the user in the loop. +10. **GitHub labels must already exist on the repo.** `gh issue create --label` will fail if the label doesn't exist. If the user has **write or triage permissions** on the `issueManagement.github.remote` repo, you can create labels first (`gh label create -R `). Otherwise, list existing labels (`gh label list -R `) and only use those, or omit labels entirely if none match. diff --git a/openclaw-skill/references/configuration.md b/openclaw-skill/references/configuration.md new file mode 100644 index 00000000..a37a27fa --- /dev/null +++ b/openclaw-skill/references/configuration.md @@ -0,0 +1,144 @@ +# Configuration + +## Settings System + +iloom uses a layered settings system with three files: + +| File | Location | Scope | Git | +|------|----------|-------|-----| +| Global settings | `~/.config/iloom-ai/settings.json` | All projects | N/A | +| Project settings | `.iloom/settings.json` | This project | Committed | +| Local settings | `.iloom/settings.local.json` | This machine | Gitignored | + +Local overrides project, project overrides global. + +For the complete settings schema and all configuration options, see `{baseDir}/references/initialization.md`. + +### Runtime Setting Overrides + +Use `--set` to override any setting for a single command: + +```bash +bash pty:true command:"il start 42 --set mergeBehavior.mode=github-pr --set capabilities.web.basePort=4000" +``` + +The `--set` flag accepts dot notation and can be repeated. + +--- + +## Environment Variables + +### iloom Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `ILOOM_DEBUG` | Enable debug logging | `false` | +| `ILOOM_SHELL` | Override shell detection | Auto-detect | +| `ILOOM_SETTINGS_PATH` | Override settings file location | `~/.config/iloom-ai/settings.json` | +| `ILOOM_NO_COLOR` | Disable colored output | `false` | +| `ILOOM_DEV_SERVER_TIMEOUT` | Dev server startup timeout (ms) | `180000` | +| `ILOOM_UPDATE_CACHE_TIMEOUT_MINS` | Update check cache TTL (minutes) | `60` | + +### Issue Tracker Variables + +| Variable | Description | Required For | +|----------|-------------|-------------| +| `LINEAR_API_TOKEN` | Linear API authentication | Linear integration | +| `JIRA_HOST` | Jira instance URL | Jira integration | +| `JIRA_USERNAME` | Jira username | Jira integration | +| `JIRA_API_TOKEN` | Jira API token | Jira integration | +| `JIRA_PROJECT_KEY` | Jira project key | Jira integration | + +### Standard Variables + +| Variable | Description | +|----------|-------------| +| `CI` | When `true`, disables interactive prompts | +| `CLAUDE_API_KEY` | Claude API key (if not using Claude CLI auth) | + +--- + +## Global CLI Flags + +Available on all commands: + +| Flag | Description | +|------|-------------| +| `--help`, `-h` | Display command help | +| `--version`, `-v` | Display iloom version | +| `--debug` | Enable debug output | +| `--no-color` | Disable colored output | +| `--set ` | Override any setting (repeatable) | + +--- + +## il projects + +List all configured iloom projects. + +```bash +bash pty:true command:"il projects --json" +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--json` | boolean | `false` | Output as JSON array | + +### JSON Output + +```json +[ + { + "configuredAt": "2026-01-15T10:00:00.000Z", + "projectPath": "/path/to/project", + "projectName": "my-project", + "activeLooms": 3, + "capabilities": ["web", "cli"] + } +] +``` + +--- + +## il update + +Update iloom CLI to the latest version. + +```bash +bash pty:true command:"il update" +bash pty:true command:"il update --dry-run" +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--dry-run` | boolean | `false` | Check for updates without installing | + +Only works for globally installed iloom (`npm install -g`). + +--- + +## il feedback + +Submit bug reports or feature requests to the iloom repository. + +```bash +bash pty:true command:"il feedback 'The rebase command fails on merge commits' --body 'Steps to reproduce...' --json" +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `` | positional | β€” | Feedback description (>30 chars, >3 words) | +| `--body ` | string | β€” | Detailed body text | +| `--json` | boolean | `false` | Output as JSON | + +--- + +## il contribute + +Fork, clone, and set up the iloom repository for contribution. + +```bash +bash pty:true command:"il contribute" +``` + +No flags. Interactive setup process. diff --git a/openclaw-skill/references/core-workflow.md b/openclaw-skill/references/core-workflow.md new file mode 100644 index 00000000..bf55c61b --- /dev/null +++ b/openclaw-skill/references/core-workflow.md @@ -0,0 +1,252 @@ +# Core Workflow Commands + +## il init + +Initialize iloom for a project. This is always the first iloom command to run. + +### Prerequisites + +1. Project directory must exist with a package manager initialized (`pnpm init`, `npm init`, `cargo init`, etc.) +2. Git repository must be initialized (`git init`) with at least one commit +3. A GitHub remote should be configured (`git remote add origin ...`) + +### Usage + +```bash +# Interactive setup wizard (foreground only, do NOT use background mode) +bash pty:true command:"il init" + +# With a natural language instruction +bash pty:true command:"il init 'set IDE to windsurf'" +``` + +### Behavior + +- Creates `.iloom/` directory with `settings.json` and `settings.local.json` +- Launches Claude Code for guided configuration +- Detects project capabilities (web, CLI, library) +- Configures issue tracker (GitHub, Linear, or Jira) +- Sets merge behavior, IDE preferences, and workflow settings +- Marks the project as configured in `~/.config/iloom-ai/projects/` + +### Important + +- **Must run in foreground** β€” this is an interactive wizard +- **Requires PTY** β€” Claude Code needs a terminal +- Only needs to run once per project + +--- + +## il start + +Create an isolated loom workspace for an issue, PR, or branch. + +**Aliases:** `new`, `create`, `up` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[identifier]` | positional | β€” | Issue number, PR number (`pr/123`), branch name, or description text | +| `--one-shot ` | enum | `default` | Automation mode: `default`, `noReview`, `bypassPermissions` | +| `--yolo` | boolean | `false` | Shorthand for `--one-shot=bypassPermissions` | +| `--claude` / `--no-claude` | boolean | `true` | Enable/disable Claude integration | +| `--code` / `--no-code` | boolean | `true` | Enable/disable VS Code opening | +| `--dev-server` / `--no-dev-server` | boolean | `true` | Enable/disable dev server | +| `--terminal` / `--no-terminal` | boolean | `false` | Enable/disable terminal tab | +| `--child-loom` / `--no-child-loom` | boolean | β€” | Force child/independent loom (skips prompt) | +| `--body ` | string | β€” | Issue body text (skips AI enhancement) | +| `--json` | boolean | `false` | Output result as JSON | + +### Examples + +```bash +# Start a loom for issue #42 in autonomous mode, no VS Code, JSON output +bash pty:true background:true command:"il start 42 --yolo --no-code --json" + +# Start a loom for a PR +bash pty:true background:true command:"il start pr/99 --yolo --no-code --json" + +# Create a loom from a description (auto-creates GitHub issue) +bash pty:true background:true command:"il start 'Add dark mode support to the settings page' --yolo --no-code --json" + +# Force independent loom (skip child loom prompt) +bash pty:true background:true command:"il start 42 --yolo --no-code --no-child-loom --json" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses and execution mode guidance. + +### JSON Output + +```json +{ + "id": "string", + "path": "/path/to/worktree", + "branch": "feat/42-feature-name", + "port": 3042, + "type": "issue", + "identifier": 42, + "title": "Feature name" +} +``` + +--- + +## il finish + +Validate, commit, merge, and clean up a loom workspace. + +**Aliases:** `dn` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[identifier]` | positional | β€” | Auto-detected from current directory if omitted | +| `-f, --force` | boolean | `false` | Skip confirmation prompts | +| `-n, --dry-run` | boolean | `false` | Preview actions without executing | +| `--pr ` | number | β€” | Treat input as PR number | +| `--skip-build` | boolean | `false` | Skip post-merge build verification | +| `--no-browser` | boolean | `false` | Skip opening PR in browser | +| `--cleanup` / `--no-cleanup` | boolean | β€” | Explicit cleanup decision (skips prompt) | +| `--json` | boolean | `false` | Output result as JSON | +| `--json-stream` | boolean | `false` | Stream JSONL progress output | + +### Examples + +```bash +# Fully autonomous finish from within a loom directory (background recommended β€” can take 1-2+ min) +bash pty:true background:true command:"il finish --force --cleanup --no-browser --json-stream" + +# Dry run to preview what would happen +bash pty:true command:"il finish --dry-run" + +# Finish a specific issue +bash pty:true background:true command:"il finish 42 --force --cleanup --no-browser --json-stream" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses and execution mode guidance. + +### JSON Output + +```json +{ + "success": true, + "type": "issue", + "identifier": 42, + "operations": [ + { "type": "validation", "message": "...", "success": true }, + { "type": "commit", "message": "...", "success": true }, + { "type": "merge", "message": "...", "success": true } + ], + "prUrl": "https://github.com/owner/repo/pull/99", + "cleanupResult": { ... } +} +``` + +--- + +## il cleanup + +Remove one or more loom workspaces without merging. + +**Aliases:** `remove`, `clean` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[identifier]` | positional | β€” | Branch name or work item identifier | +| `-l, --list` | boolean | `false` | List all worktrees (informational) | +| `-a, --all` | boolean | `false` | Remove all worktrees | +| `-i, --issue ` | number | β€” | Cleanup by issue number | +| `-f, --force` | boolean | `false` | Skip confirmations and force removal | +| `--dry-run` | boolean | `false` | Show what would be done | +| `--json` | boolean | `false` | Output result as JSON | +| `--defer ` | number | β€” | Wait before cleanup (milliseconds) | + +### Examples + +```bash +# Remove a specific loom by issue number +bash pty:true command:"il cleanup --issue 42 --force --json" + +# Remove all looms +bash pty:true command:"il cleanup --all --force --json" + +# List worktrees without removing +bash pty:true command:"il cleanup --list" + +# Dry run +bash pty:true command:"il cleanup --issue 42 --dry-run" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses. + +### JSON Output + +```json +{ + "identifier": "42", + "success": true, + "dryRun": false, + "operations": [ + { "type": "worktree", "success": true, "message": "...", "deleted": true }, + { "type": "branch", "success": true, "message": "...", "deleted": true } + ], + "errors": [], + "rollbackRequired": false +} +``` + +--- + +## il list + +Display active loom workspaces. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--json` | boolean | `false` | Output as JSON array | +| `--finished` | boolean | `false` | Show only finished looms | +| `--all` | boolean | `false` | Show both active and finished looms | +| `--global` | boolean | `false` | Show looms from all projects | +| `--children` | boolean | `false` | Include child issues and child looms | + +### Examples + +```bash +# List active looms as JSON +bash pty:true command:"il list --json" + +# List all looms (active + finished) +bash pty:true command:"il list --all --json" + +# List looms across all projects +bash pty:true command:"il list --global --json" +``` + +### No Interactive Prompts + +This is a read-only command with no interactive prompts. + +### JSON Output + +Returns an array of loom objects: + +```json +[ + { + "name": "feat/42-dark-mode", + "worktreePath": "/path/to/worktree", + "branch": "feat/42-dark-mode", + "type": "issue", + "issue_numbers": [42], + "isMainWorktree": false, + "status": "active", + "isChildLoom": false + } +] +``` diff --git a/openclaw-skill/references/development-commands.md b/openclaw-skill/references/development-commands.md new file mode 100644 index 00000000..cdce335d --- /dev/null +++ b/openclaw-skill/references/development-commands.md @@ -0,0 +1,301 @@ +# Development Commands + +Commands for working within an active loom workspace. + +## il spin + +Launch Claude CLI with auto-detected loom context. + +**Aliases:** `ignite` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--one-shot ` | enum | `default` | Automation mode: `default`, `noReview`, `bypassPermissions` | +| `--yolo` | boolean | `false` | Shorthand for `--one-shot=bypassPermissions` | +| `-p, --print` | boolean | `false` | Headless mode for CI/CD (implies `bypassPermissions`) | +| `--output-format ` | enum | β€” | Output format: `json`, `stream-json`, `text` (requires `--print`) | +| `--verbose` | boolean | β€” | Verbose output (requires `--print`) | +| `--json` | boolean | `false` | Final result as JSON (requires `--print`) | +| `--json-stream` | boolean | `false` | Stream JSONL output (requires `--print`) | + +### Examples + +```bash +# Launch Claude in background with autonomous mode +bash pty:true background:true command:"il spin --yolo" + +# Headless mode with JSON output +bash pty:true command:"il spin --yolo --print --output-format json" + +# Monitor background session +process action:log sessionId:XXX +process action:poll sessionId:XXX +``` + +See `{baseDir}/references/non-interactive-patterns.md` for execution mode guidance. + +--- + +## il commit + +Commit all uncommitted files with issue reference trailer. + +**Aliases:** `c` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `-m, --message ` | string | β€” | Custom commit message (skips Claude generation) | +| `--fixes` | boolean | `false` | Use "Fixes #N" trailer instead of "Refs #N" | +| `--no-review` | boolean | `false` | Skip commit message review prompt | +| `--json` | boolean | `false` | Output result as JSON (implies `--no-review`) | +| `--json-stream` | boolean | `false` | Stream JSONL progress output | +| `--wip-commit` | boolean | `false` | Quick WIP commit: skip validations and pre-commit hooks | + +### Examples + +```bash +# Non-interactive commit with AI-generated message (background recommended β€” Claude generates message) +bash pty:true background:true command:"il commit --no-review --json-stream" + +# Commit with custom message +bash pty:true command:"il commit -m 'fix: resolve auth timeout' --json" + +# Quick WIP commit (skips hooks and validation) +bash pty:true command:"il commit --wip-commit --json" + +# Commit that closes the issue +bash pty:true command:"il commit --fixes --no-review --json" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses and execution mode guidance. + +### JSON Output + +```json +{ + "success": true, + "commitHash": "abc1234", + "message": "fix: resolve auth timeout\n\nRefs #42", + "filesChanged": 3, + "issueNumber": 42, + "trailerType": "Refs" +} +``` + +--- + +## il rebase + +Rebase current loom branch on main with AI-assisted conflict resolution. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `-f, --force` | boolean | `false` | Force rebase even in edge cases | +| `-n, --dry-run` | boolean | `false` | Simulate rebase without changes | +| `--json-stream` | boolean | `false` | Stream JSONL progress output | + +### Examples + +```bash +# Rebase with force and progress streaming (background recommended β€” Claude may resolve conflicts) +bash pty:true background:true command:"il rebase --force --json-stream" + +# Dry run +bash pty:true command:"il rebase --dry-run" +``` + +### Behavior + +- Detects uncommitted changes and throws if found (commit first) +- Claude assists with conflict resolution if conflicts arise +- Post-rebase: installs dependencies and runs build (non-blocking) +- Use `--json-stream` for incremental progress visibility + +--- + +## il build + +Run the build script for the current workspace. + +```bash +bash pty:true command:"il build" +``` + +No flags. Runs in foreground. No JSON output. + +--- + +## il test + +Run the test script for the current workspace. + +```bash +bash pty:true command:"il test" +``` + +No flags. Runs in foreground. No JSON output. + +--- + +## il lint + +Run the lint script for the current workspace. + +```bash +bash pty:true command:"il lint" +``` + +No flags. Runs in foreground. No JSON output. + +--- + +## il compile + +Run the TypeScript compiler check for the current workspace. + +**Aliases:** `typecheck` + +```bash +bash pty:true command:"il compile" +``` + +No flags. Runs in foreground. No JSON output. + +--- + +## il dev-server + +Start the development server for a workspace. + +**Aliases:** `dev` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[identifier]` | positional | β€” | Work item identifier, PR number, or branch name (auto-detected) | +| `--json` | boolean | `false` | Output result as JSON | + +### Examples + +```bash +# Start dev server (auto-detect workspace) +bash pty:true command:"il dev-server --json" + +# Start dev server for specific issue +bash pty:true command:"il dev-server 42 --json" +``` + +### JSON Output + +```json +{ + "status": "started", + "url": "http://localhost:3042", + "port": 3042, + "pid": 12345, + "message": "Dev server started" +} +``` + +Port is calculated as `basePort + issue/PR number` (default base: 3000). + +--- + +## il shell + +Open an interactive shell with workspace environment variables loaded. + +**Aliases:** `terminal` + +```bash +bash pty:true command:"il shell" +``` + +No flags. Opens a subshell with the loom's `.env` variables injected. + +--- + +## il open + +Open the loom in browser (web projects) or run the configured CLI tool. + +**Aliases:** `run` + +```bash +bash pty:true command:"il open" +bash pty:true command:"il open 42" +``` + +Accepts an optional `[identifier]` positional argument. + +--- + +## il vscode + +Open the workspace in VS Code and install the iloom extension. + +```bash +bash pty:true command:"il vscode" +bash pty:true command:"il vscode --no-wait" +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--no-wait` | boolean | `false` | Don't wait for VS Code to close | + +--- + +## il summary + +Generate a Claude session summary for a loom. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[identifier]` | positional | β€” | Work item identifier (auto-detected from any tracker) | +| `--with-comment` | boolean | `false` | Post summary as comment to issue/PR | +| `--json` | boolean | `false` | Output result as JSON | + +### Examples + +```bash +# Generate summary for current loom +bash pty:true command:"il summary --json" + +# Generate and post as comment +bash pty:true command:"il summary --with-comment --json" +``` + +### JSON Output + +```json +{ + "summary": "## Session Summary\n...", + "sessionId": "uuid", + "branchName": "feat/42-feature", + "loomType": "issue", + "issueNumber": 42 +} +``` + +--- + +## il recap + +Get the recap (decisions, insights, risks, assumptions) for a loom. + +```bash +bash pty:true command:"il recap --json" +``` + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--json` | boolean | `false` | Output result as JSON | diff --git a/openclaw-skill/references/initialization.md b/openclaw-skill/references/initialization.md new file mode 100644 index 00000000..66176dea --- /dev/null +++ b/openclaw-skill/references/initialization.md @@ -0,0 +1,285 @@ +# Initialization (Manual Setup) + +How to set up iloom for a project without using the interactive `il init` wizard. + +## When to Use This + +Use manual initialization instead of `il init` when: +- You are an AI agent and cannot reliably drive the interactive wizard +- You want deterministic, reproducible setup +- You need to initialize many projects quickly + +## Prerequisites + +1. The project must be a **git repository** with at least one commit +2. The project must have a **remote** configured (for GitHub issue tracking) +3. iloom CLI must be installed (`il --version` to verify) + +## Step-by-Step Setup + +### 1. Create the `.iloom` directory + +```bash +mkdir -p /.iloom +``` + +### 2. Create `.iloom/settings.json` (project settings β€” committed to git) + +This file contains shared project configuration. At minimum: + +```json +{ + "mainBranch": "main" +} +``` + +A typical project setup: + +```json +{ + "mainBranch": "main", + "colors": { + "terminal": true, + "vscode": true + }, + "mergeBehavior": { + "mode": "github-draft-pr" + } +} +``` + +### 3. Create `.iloom/settings.local.json` (personal settings β€” gitignored) + +This file contains per-developer preferences that should NOT be committed: + +```json +{ + "workflows": { + "issue": { + "permissionMode": "acceptEdits" + } + } +} +``` + +### 4. Configure GitHub Remote (Fork Workflows) + +If the project has **multiple git remotes** (e.g., a fork workflow), configure which remote iloom uses for issue tracking vs. pushing. **Do not guess β€” ask the user.** + +Add to `.iloom/settings.local.json` (not `settings.json`, since this is per-developer): + +```json +{ + "issueManagement": { + "github": { + "remote": "upstream" + } + }, + "mergeBehavior": { + "remote": "origin" + } +} +``` + +**Standard convention:** `origin` = your fork, `upstream` = the original repo. iloom assumes `origin` is yours by default. + +**Understanding the two settings:** +- **`issueManagement.github.remote`** β€” The canonical GitHub repository for all GitHub operations: listing/creating issues, **opening PRs**, commenting, closing issues. PRs are opened against this repo. +- **`mergeBehavior.remote`** β€” The remote iloom pushes branches to. In a fork workflow, branches are pushed here, then a cross-fork PR is opened on the `issueManagement` repo. + +In fork workflows, issues and PRs live on `upstream` while you push branches to `origin`. + +### 5. Update `.gitignore` + +Ensure local/personal files are not tracked: + +```bash +# Add to .gitignore +echo '.iloom/settings.local.json' >> .gitignore +echo '.vscode/settings.json' >> .gitignore # Only if colors.vscode is true +``` + +### 6. Validate the configuration + +Run any iloom command to verify settings are loaded: + +```bash +il list --json +``` + +If settings are valid, this returns a JSON array of looms. If there are errors, iloom will report them. + +### 7. (Optional) Validate against JSON Schema + +The settings JSON schema is bundled at `/dist/schema/settings.schema.json`. You can validate your settings programmatically: + +```bash +# Using ajv-cli (npm install -g ajv-cli) +ajv validate -s "$(dirname $(which il))/../dist/schema/settings.schema.json" -d .iloom/settings.json + +# Or using node inline +node -e " +const schema = require('$(dirname $(which il))/../dist/schema/settings.schema.json'); +const settings = require('./.iloom/settings.json'); +// Basic structural check β€” schema is JSON Schema Draft 7 +console.log('Settings loaded:', Object.keys(settings)); +" +``` + +## Settings Reference + +### Layering Order (highest to lowest priority) + +1. **Local** β€” `.iloom/settings.local.json` (gitignored, per-machine) +2. **Project** β€” `.iloom/settings.json` (committed, shared with team) +3. **Global** β€” `~/.config/iloom-ai/settings.json` (all projects on this machine) + +Local overrides project, project overrides global. + +### Complete Settings Schema + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `mainBranch` | string | auto-detected | Primary branch name (`main`, `master`, etc.) | +| `sourceEnvOnStart` | boolean | `false` | Source dotenv-flow files when launching processes | +| `worktreePrefix` | string | `-looms` | Prefix for worktree directories (empty string disables) | +| `protectedBranches` | string[] | `[mainBranch, "main", "master", "develop"]` | Branches that cannot be deleted | +| `copyGitIgnoredPatterns` | string[] | `[]` | Glob patterns for gitignored files to copy to looms | + +#### `workflows` β€” Per-workflow-type settings + +Each workflow type (`issue`, `pr`, `regular`) supports: + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `permissionMode` | enum | `"default"` | `"plan"` \| `"acceptEdits"` \| `"bypassPermissions"` \| `"default"` | +| `noVerify` | boolean | `false` | Skip pre-commit hooks during commit/finish | +| `startIde` | boolean | `true` | Open IDE when starting a loom | +| `startDevServer` | boolean | `true` | Launch dev server on start | +| `startAiAgent` | boolean | `true` | Launch Claude Code on start | +| `startTerminal` | boolean | `false` | Open terminal window on start | +| `generateSummary` | boolean | `true` | Generate session summary on finish | + +#### `agents` β€” Per-agent configuration + +Available agents: `iloom-issue-analyzer`, `iloom-issue-planner`, `iloom-issue-analyze-and-plan`, `iloom-issue-complexity-evaluator`, `iloom-issue-enhancer`, `iloom-issue-implementer`, `iloom-code-reviewer`, `iloom-artifact-reviewer` + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `model` | enum | β€” | `"sonnet"` \| `"opus"` \| `"haiku"` | +| `enabled` | boolean | `true` | Whether this agent is enabled | +| `review` | boolean | `false` | Review artifacts before posting | +| `providers` | object | β€” | Map of `claude`/`gemini`/`codex` to model name strings | + +#### `plan` β€” Planning command settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `model` | enum | `"opus"` | Claude model for planning | +| `planner` | enum | `"claude"` | AI provider: `"claude"` \| `"gemini"` \| `"codex"` | +| `reviewer` | enum | `"none"` | Review provider: `"claude"` \| `"gemini"` \| `"codex"` \| `"none"` | + +#### `spin` β€” Spin orchestrator settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `model` | enum | `"opus"` | Claude model for spin | + +#### `summary` β€” Session summary settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `model` | enum | `"sonnet"` | Claude model for summaries | + +#### `issueManagement` β€” Issue tracker configuration + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `provider` | enum | `"github"` | `"github"` \| `"linear"` \| `"jira"` | +| `github.remote` | string | β€” | Git remote name for GitHub operations | +| `linear.teamId` | string | β€” | Linear team identifier (e.g., `"ENG"`) | +| `linear.apiToken` | string | β€” | **Local only** β€” never commit | +| `jira.host` | string | β€” | Jira instance URL | +| `jira.username` | string | β€” | Jira username/email | +| `jira.apiToken` | string | β€” | **Local only** β€” never commit | +| `jira.projectKey` | string | β€” | Jira project key (e.g., `"PROJ"`) | + +#### `mergeBehavior` β€” How looms are merged + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `mode` | enum | `"local"` | `"local"` \| `"github-pr"` \| `"github-draft-pr"` | +| `remote` | string | β€” | Git remote for PR creation | +| `autoCommitPush` | boolean | `true` (draft PR) | Auto-commit and push after code review | + +#### `ide` β€” Editor configuration + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `type` | enum | `"vscode"` | `"vscode"` \| `"cursor"` \| `"webstorm"` \| `"sublime"` \| `"intellij"` \| `"windsurf"` \| `"antigravity"` | + +#### `colors` β€” Visual identification + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `terminal` | boolean | `true` | Terminal background colors per branch (macOS only) | +| `vscode` | boolean | `false` | VSCode title bar colors per branch | + +#### `attribution` + +| Value | Description | +|-------|-------------| +| `"off"` | Never show iloom attribution | +| `"upstreamOnly"` | Only for external/open-source repos (default) | +| `"on"` | Always show attribution | + +#### `git` β€” Git operation settings + +| Setting | Type | Default | Description | +|---------|------|---------|-------------| +| `commitTimeout` | number | `60000` | Timeout (ms) for git commit operations (1s–600s) | + +## Example: Full Configuration + +**`.iloom/settings.json`** (committed): +```json +{ + "mainBranch": "main", + "mergeBehavior": { + "mode": "github-draft-pr" + }, + "issueManagement": { + "provider": "github" + }, + "colors": { + "terminal": true, + "vscode": true + }, + "agents": { + "iloom-issue-implementer": { "model": "opus" }, + "iloom-code-reviewer": { + "providers": { "gemini": "gemini-3-pro-preview" } + }, + "iloom-artifact-reviewer": { + "enabled": true, + "providers": { "gemini": "gemini-3-pro-preview" } + } + }, + "attribution": "on" +} +``` + +**`.iloom/settings.local.json`** (gitignored): +```json +{ + "workflows": { + "issue": { + "permissionMode": "bypassPermissions", + "noVerify": true, + "startIde": false, + "startAiAgent": true, + "startTerminal": false + } + } +} +``` diff --git a/openclaw-skill/references/non-interactive-patterns.md b/openclaw-skill/references/non-interactive-patterns.md new file mode 100644 index 00000000..bfb9e8c4 --- /dev/null +++ b/openclaw-skill/references/non-interactive-patterns.md @@ -0,0 +1,266 @@ +# Non-Interactive Patterns + +How to run iloom commands autonomously without hitting interactive prompts. + +## Execution Modes + +iloom commands fall into three categories based on their execution requirements: + +### No PTY needed (plain exec) + +These commands return clean JSON output and complete quickly. No PTY or background mode required: + +```bash +bash command:"il list --json" +bash command:"il issues --json" +bash command:"il projects --json" +bash command:"il recap --json" +bash command:"il --version" +bash command:"il start --no-claude --no-code --no-dev-server --no-terminal --json" +bash command:"il cleanup --issue 42 --force --json" +bash command:"il build" +bash command:"il test" +bash command:"il lint" +bash command:"il compile" +bash command:"il add-issue 'description' --json" +``` + +### Background mode required (long-running, launches Claude or extended ops) + +These commands spawn Claude Code sessions or run extended operations that can take 1-10+ minutes. Always use `background:true`: + +```bash +bash background:true command:"il plan --yolo --print --json-stream" +bash background:true command:"il spin --yolo --print --json-stream" +bash background:true command:"il start --yolo --no-code --no-terminal --json" # with Claude (default) +bash background:true command:"il summary --json" +bash background:true command:"il enhance --no-browser --json" +bash background:true command:"il commit --no-review --json-stream" +bash background:true command:"il finish --force --cleanup --no-browser --json-stream" +bash background:true command:"il rebase --force --json-stream" +``` + +**Why `--print --json-stream` for `plan` and `spin`?** +- `--print` enables headless/non-interactive output mode +- `--json-stream` streams JSONL incrementally so you can monitor progress via `process action:poll` +- Without `--json-stream`, `--print` buffers ALL output until completion (no visibility into what Claude is doing) +- These commands can easily run 3-10+ minutes with Opus analyzing a codebase β€” foreground timeouts will kill them + +**Why `--json-stream` for `commit`, `finish`, and `rebase`?** +- `commit` uses Claude to generate commit messages β€” can take 10-30s +- `finish` validates, commits, merges, and may trigger builds β€” can take 1-2+ minutes +- `rebase` may invoke Claude for conflict resolution β€” duration is unpredictable + +### Foreground PTY only (interactive, not for AI agents) + +These require a real terminal and human interaction: + +| Command | Note | +|---------|------| +| `il init` | Interactive wizard β€” **use manual setup instead** (see `{baseDir}/references/initialization.md`) | +| `il shell` | Opens interactive subshell | + +--- + +## Session Lifecycle (Background Commands) + +```bash +# 1. Start the command in background +bash background:true command:"il start 42 --yolo --no-code --no-terminal --json" +# Returns: sessionId + +# 2. Check if still running +process action:poll sessionId:XXX + +# 3. View output / progress +process action:log sessionId:XXX + +# 4. Send input if the agent asks a question +process action:submit sessionId:XXX data:"yes" + +# 5. Send raw data without newline +process action:write sessionId:XXX data:"y" + +# 6. Terminate if needed (YOUR sessions only β€” see warning below) +process action:kill sessionId:XXX +``` + +> ⚠️ **CRITICAL: Never kill a session you did not start.** +> +> When listing background processes, you may see sessions from other workflows β€” the user's own planning sessions, other agents, or prior conversations. These are **not yours to manage**. Only kill sessions you explicitly launched in the current workflow. If you're unsure whether a session is yours, **ask the user** before terminating it. Killing someone else's long-running `plan` or `spin` session destroys work in progress that cannot be recovered. + +--- + +## Decision Bypass Map + +Every interactive prompt in iloom and the flag(s) that bypass it: + +| Command | Prompt | Bypass Flag(s) | +|---------|--------|---------------| +| `start` | "Enter issue number..." | Provide `[identifier]` argument | +| `start` | "Create as a child loom?" | `--child-loom` or `--no-child-loom` | +| `start` | "bypassPermissions warning" | Already implied by `--yolo`; or `--no-claude` | +| `start` | Opens terminal window | `--no-terminal` | +| `finish` | "Clean up worktree?" | `--cleanup` or `--no-cleanup` | +| `finish` | Commit message review | `--force` | +| `finish` | General confirmations | `--force` | +| `cleanup` | "Remove this worktree?" | `--force` | +| `cleanup` | "Remove N worktree(s)?" | `--force` | +| `commit` | Commit message review | `--no-review` or `--json` | +| `enhance` | "Press q or key to view..." | `--no-browser` or `--json` | +| `enhance` | First-run setup | `--json` | +| `add-issue` | "Press key to view in browser" | `--json` | +| `add-issue` | First-run setup | `--json` | + +--- + +## Recommended Autonomous Flag Combinations + +### Full Autonomous Start (create workspace) + +```bash +bash command:"il start --yolo --no-code --no-claude --no-dev-server --no-terminal --json" +``` + +- `--yolo`: bypass all permission prompts +- `--no-code`: don't open VS Code +- `--no-claude`: don't launch Claude (use when you want to spin separately) +- `--no-dev-server`: skip dev server +- `--no-terminal`: don't open a terminal window +- `--json`: structured output + +### Full Autonomous Start (with Claude) + +```bash +bash background:true command:"il start --yolo --no-code --no-terminal --json" +``` + +- Same as above but launches Claude β€” needs `background:true` + +### Full Autonomous Finish (merge and cleanup) + +```bash +bash background:true command:"il finish --force --cleanup --no-browser --json-stream" +# Monitor: process action:poll sessionId:XXX +``` + +- `--force`: skip all confirmations +- `--cleanup`: auto-cleanup worktree +- `--no-browser`: don't open browser +- `--json-stream`: stream progress incrementally +- `background:true`: finish can take 1-2+ minutes (commit, merge, build verification) + +### Headless Planning + +```bash +bash background:true command:"il plan --yolo --print --json-stream 'Description of work to plan'" +# Monitor: process action:poll sessionId:XXX +# Full log: process action:log sessionId:XXX +``` + +- `--yolo`: autonomous mode (**requires a prompt argument or issue identifier**) +- `--print`: headless output +- `--json-stream`: stream JSONL incrementally (visible via poll/log) +- `background:true`: **required** β€” planning sessions can run 3-10+ minutes + +### Non-Interactive Commit + +```bash +bash background:true command:"il commit --no-review --json-stream" +# Monitor: process action:poll sessionId:XXX +``` + +- `--no-review`: skip message review +- `--json-stream`: stream progress (Claude generates commit message) + +### Non-Interactive Rebase + +```bash +bash background:true command:"il rebase --force --json-stream" +# Monitor: process action:poll sessionId:XXX +``` + +- `--force`: force rebase even in edge cases +- `--json-stream`: stream progress (Claude assists with conflict resolution if needed) + +### Quick Cleanup + +```bash +bash command:"il cleanup --issue --force --json" +``` + +- `--force`: skip confirmation +- `--json`: structured output + +--- + +## JSON Output Commands + +Commands that support `--json` for machine-parseable output: + +| Command | JSON Flag | Notes | +|---------|-----------|-------| +| `il start` | `--json` | Returns workspace metadata | +| `il finish` | `--json-stream` | Stream progress (mutually exclusive with `--json`) | +| `il cleanup` | `--json` | Returns cleanup results | +| `il list` | `--json` | Returns array of loom objects | +| `il commit` | `--json-stream` | Stream progress (mutually exclusive with `--json`, implies `--no-review`) | +| `il issues` | `--json` | Returns array of issues/PRs | +| `il add-issue` | `--json` | Returns created issue | +| `il enhance` | `--json` | Returns enhancement result | +| `il summary` | `--json` | Returns summary text and metadata | +| `il recap` | `--json` | Returns recap data | +| `il dev-server` | `--json` | Returns server status | +| `il projects` | `--json` | Returns project list | +| `il rebase` | `--json-stream` | Stream progress during rebase | +| `il plan` | `--json` | Returns planning result (requires `--print`) | +| `il spin` | `--json` | Returns result (requires `--print`) | + +--- + +## Auto-Notify on Completion + +For long-running background tasks, append a wake trigger so OpenClaw gets notified when iloom finishes: + +```bash +bash background:true command:"il start 42 --yolo --no-code --no-terminal --json && openclaw system event --text 'Done: Loom created for issue #42' --mode now" +``` + +This triggers an immediate wake event instead of waiting for the next heartbeat. + +--- + +## Error Handling + +### Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | General error | +| `130` | User aborted (e.g., Ctrl+C during commit review) | + +### JSON Error Format + +When `--json` is used and a command fails: + +```json +{ + "success": false, + "error": "Error message describing what went wrong" +} +``` + +### Fallback: process action:submit + +If a command hits an unexpected prompt that can't be bypassed with flags, use `process action:submit` to send input: + +```bash +# If a command unexpectedly asks for confirmation +process action:submit sessionId:XXX data:"y" + +# If it asks for text input +process action:submit sessionId:XXX data:"some value" +``` + +This should be rare β€” the flag combinations above cover all known interactive prompts. If you encounter an undocumented prompt, submit a reasonable default and note it for future reference. diff --git a/openclaw-skill/references/planning-and-issues.md b/openclaw-skill/references/planning-and-issues.md new file mode 100644 index 00000000..a51dd75f --- /dev/null +++ b/openclaw-skill/references/planning-and-issues.md @@ -0,0 +1,208 @@ +# Planning and Issue Management + +## il plan + +Launch an interactive AI planning session to decompose features into child issues. + +**When to use:** Whenever the user has an idea for an improvement, new feature, or wants to break down work into implementable tasks. This is the primary ideation tool β€” always prefer `il plan` over manually creating issues. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `[prompt]` | positional | β€” | Planning topic or issue identifier (e.g., `#123`, `ENG-456`) | +| `--model ` | string | `opus` | Claude model: `opus`, `sonnet`, `haiku` | +| `--yolo` | boolean | `false` | Autonomous mode (bypass all permission prompts) | +| `--planner ` | enum | `claude` | AI planner: `claude`, `gemini`, `codex` | +| `--reviewer ` | enum | `none` | AI reviewer: `claude`, `gemini`, `codex`, `none` | +| `-p, --print` | boolean | `false` | Headless mode for CI/CD (implies `--yolo`) | +| `--output-format ` | enum | β€” | Output format: `json`, `stream-json`, `text` (requires `--print`) | +| `--verbose` | boolean | β€” | Verbose output (requires `--print`) | +| `--json` | boolean | `false` | Final result as JSON (requires `--print`) | +| `--json-stream` | boolean | `false` | Stream JSONL output (requires `--print`) | + +### Modes + +**Fresh Planning Mode** β€” prompt describes the work to plan: +```bash +# Autonomous planning with prompt (required for --yolo) +bash pty:true background:true command:"il plan --yolo --print --json-stream 'Add authentication to the API'" + +# Headless planning with JSON output +bash pty:true command:"il plan --yolo --print --output-format json 'Add authentication to the API'" +``` + +> **Note:** `--yolo` mode requires either a **prompt argument** (description string) or an **issue identifier**. Without one, the command fails immediately. + +**Issue Decomposition Mode** β€” issue identifier provided: +```bash +# Decompose existing issue #123 into child tasks +bash pty:true background:true command:"il plan '#123' --yolo" + +# Linear issue decomposition +bash pty:true background:true command:"il plan 'ENG-456' --yolo" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for execution mode guidance and session lifecycle. + +### JSON Output (with `--print`) + +```json +{ + "success": true, + "output": "[Claude planning response]" +} +``` + +### Capabilities + +The planner has access to: +- Issue management (create issues, child issues, dependencies, comments) +- Code exploration (Read, Glob, Grep) +- Web search and fetch +- Git commands (read-only: status, log, branch, remote, diff, show) + +--- + +## il add-issue + +Create a new issue with AI-enhanced description. + +**Aliases:** `a` + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `` | positional | β€” | Issue title/description (required, >30 chars, >3 words unless `--body` used) | +| `--body ` | string | β€” | Issue body text (bypasses length/word validation) | +| `--json` | boolean | `false` | Output result as JSON (non-interactive) | + +### Examples + +```bash +# Create issue with AI enhancement (non-interactive) +bash pty:true command:"il add-issue 'Add dark mode toggle to settings page' --json" + +# Create issue with explicit body +bash pty:true command:"il add-issue 'Fix login timeout' --body 'Users report 504 errors after 30 seconds' --json" +``` + +### Behavior + +1. Validates description format +2. Runs description through AI enhancement agent (always) +3. Creates the issue on the configured tracker +4. Returns structured result in `--json` mode + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses. + +### JSON Output + +```json +{ + "url": "https://github.com/owner/repo/issues/123", + "id": 123, + "title": "Add dark mode toggle to settings page", + "created_at": "2026-02-17T12:00:00.000Z" +} +``` + +--- + +## il enhance + +Apply AI enhancement to an existing issue's description. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `` | positional | β€” | Work item identifier (required) | +| `--no-browser` | boolean | `false` | Skip browser opening prompt | +| `--author ` | string | β€” | GitHub username to tag in questions | +| `--json` | boolean | `false` | Output result as JSON (non-interactive) | + +### Examples + +```bash +# Enhance issue #42 (non-interactive) +bash pty:true command:"il enhance 42 --no-browser --json" + +# Enhance with author tagging +bash pty:true command:"il enhance 42 --author johndoe --no-browser --json" +``` + +See `{baseDir}/references/non-interactive-patterns.md` for prompt bypasses. + +### JSON Output + +```json +{ + "url": "https://github.com/owner/repo/issues/42#issuecomment-789", + "id": 789, + "title": "Issue Title", + "created_at": "2026-02-17T12:00:00.000Z", + "enhanced": true +} +``` + +The `enhanced` field is `false` if the issue already had a thorough description. + +--- + +## il issues + +List open issues and PRs from the configured tracker. + +### Flags + +| Flag | Type | Default | Description | +|------|------|---------|-------------| +| `--json` | boolean | `false` | Output as JSON array | +| `--limit ` | number | `100` | Max number of results | +| `--sprint ` | string | β€” | Filter by sprint (Jira only) | +| `--mine` | boolean | `false` | Show only my issues (Jira only) | + +### Examples + +```bash +# List all open issues as JSON +bash pty:true command:"il issues --json" + +# List with limit +bash pty:true command:"il issues --json --limit 20" + +# Jira: filter by sprint +bash pty:true command:"il issues --json --sprint 'Sprint 5'" +``` + +### Behavior + +- Always returns JSON array (no interactive output) +- Results cached for 2 minutes +- Includes both issues and PRs (PRs from GitHub regardless of issue tracker) +- Sorted by `updatedAt` descending + +### JSON Output + +```json +[ + { + "id": "123", + "title": "Fix login timeout", + "updatedAt": "2026-02-17T12:00:00.000Z", + "url": "https://github.com/owner/repo/issues/123", + "state": "open", + "type": "issue" + }, + { + "id": "99", + "title": "Add dark mode", + "updatedAt": "2026-02-16T10:00:00.000Z", + "url": "https://github.com/owner/repo/pull/99", + "state": "open", + "type": "pr" + } +] +``` diff --git a/scripts/copy-docs.ts b/scripts/copy-docs.ts index f60c7666..471b7576 100644 --- a/scripts/copy-docs.ts +++ b/scripts/copy-docs.ts @@ -1,4 +1,4 @@ -import { copyFile } from 'fs/promises' +import { copyFile, cp } from 'fs/promises' import path from 'path' import { existsSync } from 'fs' @@ -14,6 +14,16 @@ async function copyDocs() { } else { console.warn(`⚠ README.md not found at ${readmeSrc} - skipping copy`) } + + // Copy openclaw-skill/ to dist + const openclawSrc = path.join(process.cwd(), 'openclaw-skill') + const openclawDest = path.join(distDir, 'openclaw-skill') + if (existsSync(openclawSrc)) { + await cp(openclawSrc, openclawDest, { recursive: true }) + console.log(`βœ“ openclaw-skill/ copied to ${openclawDest}`) + } else { + console.warn(`⚠ openclaw-skill/ not found at ${openclawSrc} - skipping copy`) + } } copyDocs().catch(console.error) \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index c5dbe81c..767fe9d5 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -171,7 +171,7 @@ async function validateSettingsForCommand(command: Command): Promise { const commandName = command.name() // Tier 1: Commands that bypass ALL validation - const bypassCommands = ['help', 'init', 'update', 'contribute', 'telemetry'] + const bypassCommands = ['help', 'init', 'update', 'contribute', 'telemetry', 'openclaw'] if (bypassCommands.includes(commandName)) { return @@ -2213,6 +2213,24 @@ program } }) +// OpenClaw skill registration +program + .command('openclaw') + .description('Register iloom as an OpenClaw skill') + .option('-f, --force', 'Overwrite existing skill registration') + .option('-w, --workspace ', 'OpenClaw workspace name', 'workspace') + .action(async (options: { force?: boolean; workspace?: string }) => { + try { + const { OpenclawCommand } = await import('./commands/openclaw.js') + const command = new OpenclawCommand() + const result = await command.execute(options) + logger.info(result.status) + } catch (error) { + logger.error(error instanceof Error ? error.message : 'Unknown error') + process.exit(1) + } + }) + // Telemetry management commands const telemetryCmd = program .command('telemetry') diff --git a/src/commands/openclaw.ts b/src/commands/openclaw.ts new file mode 100644 index 00000000..89b0e836 --- /dev/null +++ b/src/commands/openclaw.ts @@ -0,0 +1,185 @@ +import path from 'path' +import os from 'os' +import { fileURLToPath } from 'url' +import fs from 'fs-extra' +import { getLogger } from '../utils/logger-context.js' +import { TelemetryService } from '../lib/TelemetryService.js' + +export interface OpenclawOptions { + force?: boolean + workspace?: string +} + +export interface OpenclawResult { + status: string + source: string + target: string +} + +export class OpenclawCommand { + private readonly projectRoot: string + + constructor(projectRoot?: string) { + this.projectRoot = projectRoot ?? process.cwd() + } + + async execute(options: OpenclawOptions = {}): Promise { + const logger = getLogger() + const workspace = options.workspace ?? 'workspace' + const force = options.force ?? false + + // 1. Resolve and verify openclaw-skill/ exists + const skillSourceDir = await this.resolveSkillDir() + logger.debug(`Resolved openclaw-skill directory: ${skillSourceDir}`) + + // 2. Check ~/.openclaw exists + const openclawHome = path.join(os.homedir(), '.openclaw') + if (!(await fs.pathExists(openclawHome))) { + throw new Error('OpenClaw is not installed (~/.openclaw not found)') + } + + // 3. Check workspace exists + const workspaceDir = path.join(openclawHome, workspace) + if (!(await fs.pathExists(workspaceDir))) { + throw new Error( + `Workspace '${workspace}' not found. Use --workspace to specify a different workspace.` + ) + } + + // 4. Ensure skills/ dir exists + const skillsDir = path.join(workspaceDir, 'skills') + await fs.mkdirp(skillsDir) + + // 5. Check existing target + const targetPath = path.join(skillsDir, 'iloom') + const wasAlreadyLinked = await this.handleExistingTarget(targetPath, skillSourceDir, force, workspace) + + if (wasAlreadyLinked) { + this.trackLinked({ force, wasAlreadyLinked: true, customWorkspace: workspace !== 'workspace' }) + return { + status: 'Already linked', + source: skillSourceDir, + target: targetPath, + } + } + + // 6. Create symlink + await fs.symlink(skillSourceDir, targetPath) + logger.debug(`Created symlink: ${targetPath} -> ${skillSourceDir}`) + + this.trackLinked({ force, wasAlreadyLinked: false, customWorkspace: workspace !== 'workspace' }) + + return { + status: 'Linked successfully', + source: skillSourceDir, + target: targetPath, + } + } + + /** + * Resolve the openclaw-skill directory. + * 1. Check relative to the package install location (dist/openclaw-skill/ for npm installs) + * 2. Fall back to projectRoot/openclaw-skill/ (for local dev / repo clones) + * 3. Throw if neither exists + */ + private async resolveSkillDir(): Promise { + // 1. Relative to the installed package (works for npm installs) + const __filename = fileURLToPath(import.meta.url) + const __dirname = path.dirname(__filename) + // In dist: dist/commands/openclaw.js -> walk up to dist/, then openclaw-skill/ + let packageDir = __dirname + while (packageDir !== path.dirname(packageDir)) { + const candidate = path.join(packageDir, 'openclaw-skill') + if (await fs.pathExists(candidate)) { + return candidate + } + packageDir = path.dirname(packageDir) + } + + // 2. Relative to projectRoot (for local dev / repo clones) + const devCandidate = path.join(this.projectRoot, 'openclaw-skill') + if (await fs.pathExists(devCandidate)) { + return devCandidate + } + + throw new Error( + `openclaw-skill/ directory not found. Searched from package location (${__dirname}) and project root (${this.projectRoot}).` + ) + } + + /** + * Handle an existing file/symlink at the target path. + * Returns true if already correctly linked (no action needed). + * Throws if conflict exists and --force not specified. + * Removes existing target if --force specified. + */ + private async handleExistingTarget( + targetPath: string, + expectedSource: string, + force: boolean, + workspace: string + ): Promise { + if (!(await fs.pathExists(targetPath)) && !(await this.isDeadSymlink(targetPath))) { + return false + } + + const isSymlink = await this.isSymlink(targetPath) + + if (isSymlink) { + const currentTarget = await fs.readlink(targetPath) + const resolvedCurrent = path.resolve(path.dirname(targetPath), currentTarget) + const resolvedExpected = path.resolve(expectedSource) + + if (resolvedCurrent === resolvedExpected) { + return true // Already correctly linked + } + } + + // Conflict exists + if (!force) { + const workspaceHint = workspace === 'workspace' + ? ' Use --workspace if you meant to install to a different workspace.' + : '' + const typeDesc = isSymlink ? 'a symlink pointing elsewhere' : 'a file or directory' + throw new Error( + `${targetPath} already exists as ${typeDesc}. Use --force to overwrite.${workspaceHint}` + ) + } + + // Force: remove existing + await fs.remove(targetPath) + return false + } + + private async isSymlink(filePath: string): Promise { + try { + const stats = await fs.lstat(filePath) + return stats.isSymbolicLink() + } catch { + return false + } + } + + private async isDeadSymlink(filePath: string): Promise { + try { + await fs.lstat(filePath) // lstat succeeds even for dead symlinks + const exists = await fs.pathExists(filePath) // pathExists follows the link + return !exists + } catch { + return false + } + } + + private trackLinked(props: { force: boolean; wasAlreadyLinked: boolean; customWorkspace: boolean }): void { + try { + TelemetryService.getInstance().track('openclaw.linked', { + force: props.force, + was_already_linked: props.wasAlreadyLinked, + custom_workspace: props.customWorkspace, + }) + } catch (error) { + const logger = getLogger() + logger.debug(`Telemetry error: ${error instanceof Error ? error.message : String(error)}`) + } + } +} diff --git a/src/types/telemetry.ts b/src/types/telemetry.ts index 03666733..020d83a3 100644 --- a/src/types/telemetry.ts +++ b/src/types/telemetry.ts @@ -106,6 +106,11 @@ export interface AutoSwarmCompletedProperties { phase_reached: 'plan' | 'start' | 'spin' fallback_to_normal: boolean } +export interface OpenclawLinkedProperties { + force: boolean + was_already_linked: boolean + custom_workspace: boolean +} // --- Event name β†’ properties map (for type-safe track() in downstream issues) --- export interface TelemetryEventMap { @@ -127,6 +132,7 @@ export interface TelemetryEventMap { 'init.completed': InitCompletedProperties 'auto_swarm.started': AutoSwarmStartedProperties 'auto_swarm.completed': AutoSwarmCompletedProperties + 'openclaw.linked': OpenclawLinkedProperties } export type TelemetryEventName = keyof TelemetryEventMap diff --git a/tests/commands/openclaw.test.ts b/tests/commands/openclaw.test.ts new file mode 100644 index 00000000..efad93f0 --- /dev/null +++ b/tests/commands/openclaw.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import path from 'path' +import os from 'os' +import fs from 'fs-extra' +import type { Stats } from 'fs' +import { OpenclawCommand } from '../../src/commands/openclaw.js' + +vi.mock('fs-extra') +vi.mock('../../src/utils/logger-context.js', () => ({ + getLogger: () => ({ + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }), +})) +vi.mock('../../src/lib/TelemetryService.js', () => ({ + TelemetryService: { + getInstance: () => ({ + track: vi.fn(), + }), + }, +})) + +const projectRoot = '/test/project' +const homeDir = os.homedir() +const openclawHome = path.join(homeDir, '.openclaw') +const defaultWorkspaceDir = path.join(openclawHome, 'workspace') +const skillsDir = path.join(defaultWorkspaceDir, 'skills') +const targetPath = path.join(skillsDir, 'iloom') +const skillSourceDir = path.join(projectRoot, 'openclaw-skill') + +describe('OpenclawCommand', () => { + let command: OpenclawCommand + + beforeEach(() => { + command = new OpenclawCommand(projectRoot) + }) + + describe('successful linking', () => { + it('should create symlink when everything is in place', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return false + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.symlink).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockRejectedValue(new Error('ENOENT')) + + const result = await command.execute() + + expect(result.status).toBe('Linked successfully') + expect(result.source).toBe(skillSourceDir) + expect(result.target).toBe(targetPath) + expect(fs.symlink).toHaveBeenCalledWith(skillSourceDir, targetPath) + }) + + it('should report already linked when symlink points to correct target', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => true } as unknown as Stats) + vi.mocked(fs.readlink).mockResolvedValue(skillSourceDir) + + const result = await command.execute() + + expect(result.status).toBe('Already linked') + expect(fs.symlink).not.toHaveBeenCalled() + }) + + it('should use custom workspace when specified', async () => { + const customWorkspaceDir = path.join(openclawHome, 'my-workspace') + const customSkillsDir = path.join(customWorkspaceDir, 'skills') + const customTargetPath = path.join(customSkillsDir, 'iloom') + + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === customWorkspaceDir) return true + if (p === customTargetPath) return false + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.symlink).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockRejectedValue(new Error('ENOENT')) + + const result = await command.execute({ workspace: 'my-workspace' }) + + expect(result.status).toBe('Linked successfully') + expect(result.target).toBe(customTargetPath) + expect(fs.mkdirp).toHaveBeenCalledWith(customSkillsDir) + }) + }) + + describe('error cases', () => { + it('should error when openclaw-skill/ directory is missing', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return false + return false + }) + + await expect(command.execute()).rejects.toThrow('openclaw-skill/ directory not found') + }) + + it('should error when ~/.openclaw is not installed', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return false + return false + }) + + await expect(command.execute()).rejects.toThrow('OpenClaw is not installed (~/.openclaw not found)') + }) + + it('should error when workspace does not exist', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return false + return false + }) + + await expect(command.execute()).rejects.toThrow( + "Workspace 'workspace' not found. Use --workspace to specify a different workspace." + ) + }) + + it('should error when target exists as wrong symlink without --force', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => true } as unknown as Stats) + vi.mocked(fs.readlink).mockResolvedValue('/some/other/path') + + await expect(command.execute()).rejects.toThrow('Use --force to overwrite') + await expect(command.execute()).rejects.toThrow('a symlink pointing elsewhere') + }) + + it('should error when target exists as file/directory without --force', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => false } as unknown as Stats) + + await expect(command.execute()).rejects.toThrow('a file or directory') + }) + + it('should mention --workspace when default workspace has conflict', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => false } as unknown as Stats) + + await expect(command.execute()).rejects.toThrow('--workspace') + }) + + it('should not mention --workspace when custom workspace has conflict', async () => { + const customWorkspaceDir = path.join(openclawHome, 'custom') + const customTargetPath = path.join(customWorkspaceDir, 'skills', 'iloom') + + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === customWorkspaceDir) return true + if (p === customTargetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => false } as unknown as Stats) + + try { + await command.execute({ workspace: 'custom' }) + expect.unreachable('Should have thrown') + } catch (error) { + expect((error as Error).message).toContain('Use --force to overwrite') + expect((error as Error).message).not.toContain('--workspace') + } + }) + }) + + describe('--force flag', () => { + it('should overwrite existing symlink with --force', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => true } as unknown as Stats) + vi.mocked(fs.readlink).mockResolvedValue('/some/other/path') + vi.mocked(fs.remove).mockResolvedValue(undefined) + vi.mocked(fs.symlink).mockResolvedValue(undefined) + + const result = await command.execute({ force: true }) + + expect(result.status).toBe('Linked successfully') + expect(fs.remove).toHaveBeenCalledWith(targetPath) + expect(fs.symlink).toHaveBeenCalledWith(skillSourceDir, targetPath) + }) + + it('should overwrite existing directory with --force', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return true + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockResolvedValue({ isSymbolicLink: () => false } as unknown as Stats) + vi.mocked(fs.remove).mockResolvedValue(undefined) + vi.mocked(fs.symlink).mockResolvedValue(undefined) + + const result = await command.execute({ force: true }) + + expect(result.status).toBe('Linked successfully') + expect(fs.remove).toHaveBeenCalledWith(targetPath) + }) + }) + + describe('skills directory creation', () => { + it('should create skills/ directory if it does not exist', async () => { + vi.mocked(fs.pathExists).mockImplementation(async (p: string) => { + if (p === skillSourceDir) return true + if (p === openclawHome) return true + if (p === defaultWorkspaceDir) return true + if (p === targetPath) return false + return false + }) + vi.mocked(fs.mkdirp).mockResolvedValue(undefined) + vi.mocked(fs.symlink).mockResolvedValue(undefined) + vi.mocked(fs.lstat).mockRejectedValue(new Error('ENOENT')) + + await command.execute() + + expect(fs.mkdirp).toHaveBeenCalledWith(skillsDir) + }) + }) +})