diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..a2073a79f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,84 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: "Release version (e.g. v0.5, v1.0.0)" + required: true + type: string + update_stable: + description: "Update the 'stable' tag to point to this release" + required: false + type: boolean + default: true + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Validate version format + run: | + if ! echo "${{ inputs.version }}" | grep -qE '^v[0-9]+(\.[0-9]+){1,2}$'; then + echo "::error::Version must match vMAJOR.MINOR or vMAJOR.MINOR.PATCH (e.g. v0.5, v1.0.0)" + exit 1 + fi + + - name: Check tag does not already exist + run: | + if git rev-parse "${{ inputs.version }}" >/dev/null 2>&1; then + echo "::error::Tag ${{ inputs.version }} already exists" + exit 1 + fi + + - name: Check for commits since last tag + id: changelog + run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$LAST_TAG" ]; then + RANGE="${LAST_TAG}..HEAD" + COMMIT_COUNT=$(git rev-list --count "$RANGE") + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "::error::No commits since $LAST_TAG — nothing to release" + exit 1 + fi + NOTES=$(git log "$RANGE" --pretty=format:"- %s (%h)") + else + NOTES=$(git log --pretty=format:"- %s (%h)") + fi + + # Write notes to file (multi-line safe) + echo "$NOTES" > /tmp/release-notes.md + echo "last_tag=${LAST_TAG:-none}" >> "$GITHUB_OUTPUT" + + - name: Create and push tag + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "${{ inputs.version }}" -m "Release ${{ inputs.version }}" + git push origin "${{ inputs.version }}" + + - name: Update stable tag + if: inputs.update_stable + run: | + echo "Updating stable tag → ${{ inputs.version }}" + git tag -f stable "${{ inputs.version }}" + git push origin stable --force + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ inputs.version }}" \ + --title "Kōan ${{ inputs.version }}" \ + --notes-file /tmp/release-notes.md \ + --latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8088fda51..b78613384 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,7 @@ jobs: strategy: fail-fast: true matrix: + python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.14"]') || fromJSON('["3.11", "3.14"]') }} group: - name: fast marker: "not slow" @@ -36,15 +37,15 @@ jobs: marker: "slow" split_group: 3 - name: test (${{ matrix.group.name }}) + name: test (py${{ matrix.python-version }}, ${{ matrix.group.name }}) steps: - uses: actions/checkout@v6 - - name: Set up Python 3.14 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: - python-version: "3.14" + python-version: "${{ matrix.python-version }}" allow-prereleases: true cache: 'pip' cache-dependency-path: koan/requirements.txt @@ -52,7 +53,7 @@ jobs: - name: Install dependencies run: | pip install -r koan/requirements.txt - pip install pytest pytest-split + pip install pytest pytest-split pytest-cov pytest-xdist - name: Run tests (${{ matrix.group.name }}) if: ${{ !inputs.group || matrix.group.name == inputs.group }} @@ -64,7 +65,86 @@ jobs: KOAN_TELEGRAM_CHAT_ID: "123456789" run: | if [ -n "${{ matrix.group.split_group }}" ]; then - pytest tests/ -m "${{ matrix.group.marker }}" --splits 3 --group ${{ matrix.group.split_group }} -v + pytest tests/ -m "${{ matrix.group.marker }}" --splits 3 --group ${{ matrix.group.split_group }} -v \ + -n auto --dist loadfile \ + --cov=app --cov-report=term-missing else - pytest tests/ -m "${{ matrix.group.marker }}" -v + pytest tests/ -m "${{ matrix.group.marker }}" -v \ + -n auto --dist loadfile \ + --cov=app --cov-report=term-missing fi + + - name: Upload coverage data + if: ${{ !inputs.group || matrix.group.name == inputs.group }} + uses: actions/upload-artifact@v4 + with: + name: coverage-py${{ matrix.python-version }}-${{ matrix.group.name }} + path: koan/.coverage + include-hidden-files: true + + check-coverage: + needs: test + runs-on: ubuntu-latest + timeout-minutes: 5 + name: check-coverage + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: "3.14" + allow-prereleases: true + + - name: Install coverage + run: pip install coverage + + - name: Download all coverage artifacts + uses: actions/download-artifact@v4 + with: + pattern: coverage-py* + path: coverage-parts + + - name: Combine coverage and check baselines + working-directory: koan + env: + KOAN_ROOT: ${{ github.workspace }}/koan + run: | + # Collect all .coverage files and rename for combine + i=0 + for f in ../coverage-parts/coverage-*/.coverage; do + cp "$f" ".coverage.$i" + i=$((i + 1)) + done + + coverage combine + TOTAL_COV=$(coverage report --format=total 2>/dev/null || coverage report | grep '^TOTAL' | awk '{print $NF}' | tr -d '%') + echo "Total coverage: ${TOTAL_COV}%" + + # Read baselines + BASELINE_COV=$(cat ../coverage-baseline.txt | tr -d '[:space:]') + echo "Baseline coverage: ${BASELINE_COV}%" + + # Allow 0.5% tolerance on coverage + python3 -c " + import sys + actual = float('${TOTAL_COV}') + baseline = float('${BASELINE_COV}') + tolerance = 0.5 + if actual < baseline - tolerance: + print(f'FAIL: Coverage {actual}% is below baseline {baseline}% (tolerance {tolerance}%)') + sys.exit(1) + print(f'OK: Coverage {actual}% meets baseline {baseline}% (tolerance {tolerance}%)') + " + + - name: Check test count baseline + run: | + BASELINE_COUNT=$(cat test-count-baseline.txt | tr -d '[:space:]') + echo "Test count baseline: ${BASELINE_COUNT}" + + # Sum test counts from all matrix job logs + # The test count check is best-effort from the coverage report; + # the authoritative count comes from running all tests. + # For now, this is informational — the hard enforcement is on coverage. + echo "INFO: Test count baseline is ${BASELINE_COUNT}. Monitor via PR review." diff --git a/.gitignore b/.gitignore index bb82d5290..39707c154 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ .venv/ venv/ .coverage +.coverage.* +htmlcov/ +koan/htmlcov/ # Runtime .koan-status diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 000000000..75a745c0c --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,125 @@ +# AI Policy + +> **TL;DR** — AI tools assist our workflow at every stage. Humans remain in control of every decision, every review, and every release. + +--- + +## Overview + +This document describes how artificial intelligence tools are used in the maintenance and development of this project. It is intended to be transparent with our contributors, users, and the broader open-source community about the role AI plays — and, equally importantly, the role it does **not** play. + +We believe in honest, clear communication about AI-assisted workflows. This policy will be updated as our practices evolve. + +--- + +## Our Guiding Principle + +**AI assists. Humans decide.** + +The maintainers who have been stewarding this project for years remain fully responsible for every line of code that ships. AI tools extend our capacity to review, research, and improve — they do not replace human judgment, expertise, or accountability. + +--- + +## How AI Is Used in This Project + +### 1. Code and Issue Analysis + +AI tools help us process and understand incoming issues, pull requests, and code changes at scale. This includes: + +- Summarising issue reports and identifying patterns across similar bugs +- Analysing code diffs for potential problems, regressions, or style inconsistencies +- Surfacing relevant context from the codebase, documentation, and prior discussions +- Flagging potential security concerns for human review + +This analysis is **always** used as input to human decision-making, never as a substitute for it. + +### 2. Draft Pull Requests + +AI may generate draft pull requests as a starting point for a fix, a refactor, or an improvement. These drafts: + +- Are clearly labelled as AI-generated when created +- Represent a first pass only — they are never considered complete or correct without human review +- May be substantially reworked, rejected, or replaced entirely by maintainers + +Think of these drafts the way you would think of a junior contributor's first attempt: useful raw material that still needs experienced eyes. + +### 3. Human Review of Every Pull Request + +**Every pull request — whether AI-drafted or human-authored — is reviewed by a human maintainer before it can be merged.** + +During review, maintainers actively use AI as a tool to assist their own thinking: + +- Asking AI to explain or justify specific implementation choices +- Challenging AI-generated code and requesting alternative approaches +- Using AI to research edge cases, relevant standards, or upstream behaviour +- Requesting targeted rewrites of individual sections based on review feedback + +The maintainer's judgment always takes precedence. AI answers are treated as input to be verified, not conclusions to be accepted. + +### 4. Test Coverage and Defect Detection + +AI helps us improve the quality and completeness of our test suite by: + +- Suggesting test cases for edge conditions and failure modes +- Identifying gaps in existing test coverage +- Proposing tests that target known classes of defects or security issues +- Helping reproduce and characterise reported bugs + +All suggested tests are reviewed and validated by maintainers before being committed. + +### 5. Security Review + +AI tools assist in identifying potential security issues, including: + +- Common vulnerability patterns (injection, insecure defaults, deprecated APIs, etc.) +- Dependencies with known CVEs +- Code paths that may warrant closer scrutiny + +Security findings from AI are **always** verified by a human maintainer. We do not act on AI-flagged security issues without independent assessment. + +--- + +## What AI Does Not Do + +To be explicit about the limits of AI involvement in this project: + +| ❌ AI does not… | ✅ A human maintainer does… | +|---|---| +| Approve or merge pull requests | Review and decide on every PR | +| Make architectural decisions | Own all design and direction choices | +| Triage and close issues autonomously | Assess and respond to all issues | +| Publish releases | Tag, build, and release manually | +| Represent the project publicly | Communicate on behalf of the project | + +--- + +## Releases + +Releases are performed manually by the same long-standing maintainers as always. The release process — including changelog review, version tagging, and publication — involves no AI-driven automation. Every release is initiated, supervised, and published by a human maintainer. + +AI may assist in drafting changelogs or release notes, but these are always reviewed and edited before publication. + +--- + +## Attribution and Transparency + +Where AI has played a material role in generating code or content within a pull request, we aim to note this in the PR description (e.g. via a `Generated-By` or `AI-Assisted` label or note). We do not consider AI the author of any contribution — the maintainer who reviewed and approved the work takes responsibility for it. + +--- + +## Why We Do This + +Open-source software is built on trust. Our users and downstream dependants trust us to ship correct, secure, and well-considered code. AI tools help us do that work better — but they do not change who is responsible for the outcome. + +We use AI because it makes our maintainers more effective, not because it replaces them. + +--- + +## Questions and Feedback + +If you have questions about our use of AI, or concerns about a specific pull request or change, please open an issue or start a discussion. We are committed to being open about our process. + +--- + +*Last updated: 2026-03-23* +*This policy is maintained by the project maintainers and subject to revision as AI tooling and community norms evolve.* diff --git a/CLAUDE.md b/CLAUDE.md index 726bfeeae..02b6617d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,8 +18,11 @@ make run # Start main agent loop (foreground) make awake # Start Telegram bridge (foreground) make ollama # Start full Ollama stack (ollama serve + awake + run) make dashboard # Start Flask web dashboard (port 5001) -make test # Run full test suite (pytest) +make lint # Run ruff linter (must pass before committing) +make test # Run full test suite (pytest + coverage summary) +make coverage # Run tests with detailed coverage report (HTML in htmlcov/) make say m="..." # Send test message as if from Telegram +make rename-project old=X new=Y [apply=1] # Rename a project everywhere (dry-run by default) make clean # Remove venv ``` @@ -35,6 +38,7 @@ KOAN_ROOT=/tmp/test-koan .venv/bin/pytest koan/tests/test_missions.py -v - With `runpy.run_module()` (CLI tests), patch both `app..format_and_send` **and** `app.notify.format_and_send` — `runpy` re-executes the module so the import-level binding escapes the first patch. - When `load_dotenv()` would reload env vars from `.env` (defeating `monkeypatch.delenv`), patch `app.notify.load_dotenv` too. - **Test behavior, not implementation.** Unless the project's own conventions say otherwise, tests should validate what code does (inputs → outputs, side effects, observable state), not how it does it. Mocking internal dependencies of the unit under test is fine, but tests must never read or inspect actual source code to verify whether specific code is present or absent — that couples tests to implementation text rather than behavior. Prefer asserting on return values, raised exceptions, file contents, or other observable outcomes. +- **Mock above retry_with_backoff, not below.** When testing error handling for `run_gh()`/`api()` callers, mock at the `run_gh` or `api` level — never at `app.github.subprocess.run`. Mocking subprocess.run causes `retry_with_backoff` to sleep 1+2+4s between retries, adding 7+ seconds per test. See `testing-anti-patterns.md` Anti-Pattern 6. ## Architecture @@ -52,6 +56,7 @@ Communication between processes happens through shared files in `instance/` with - **`projects_config.py`** — Project configuration loader for `projects.yaml`. `load_projects_config()`, `get_projects_from_config()`, `get_project_config()` (merged defaults + overrides), `get_project_auto_merge()`, `get_project_cli_provider()`, `get_project_models()`, `get_project_tools()`. Per-project overrides for CLI provider, model selection, and tool restrictions. `ensure_github_urls()` auto-populates `github_url` fields from git remotes at startup. - **`projects_migration.py`** — One-shot migration from env vars (`KOAN_PROJECTS`/`KOAN_PROJECT_PATH`) to `projects.yaml`. Runs at startup if `projects.yaml` doesn't exist. - **`utils.py`** — File locking (thread + file locks), config loading, atomic writes, `get_branch_prefix()`, `get_known_projects()` (projects.yaml > KOAN_PROJECTS) +- **`commit_conventions.py`** — Project commit convention detection and parsing. `get_project_commit_guidance()` reads CLAUDE.md commit-related sections or infers conventions from recent commit history. `parse_commit_subject()` extracts `COMMIT_SUBJECT:` markers from Claude output. Used by `rebase_pr.py` and `ci_queue_runner.py` to produce convention-aware commit messages. **Agent loop pipeline** (called from `run.py`): - **`iteration_manager.py`** — Per-iteration decision-making: usage refresh, mode selection, recurring injection, mission picking, project resolution. @@ -60,8 +65,9 @@ Communication between processes happens through shared files in `instance/` with - **`contemplative_runner.py`** — Contemplative session runner (probability roll, prompt building, CLI invocation) - **`quota_handler.py`** — Quota exhaustion detection from CLI output; parses reset times, creates pause state, writes journal entries - **`prompt_builder.py`** — Agent prompt assembly for the agent loop -- **`pr_review_learning.py`** — Extracts actionable lessons from human PR reviews using Claude CLI (lightweight model). Fetches review data from GitHub, sends raw comments to Claude for natural-language analysis, and persists new lessons to `memory/projects/{name}/learnings.md` (write-once, read-many). Uses content-hash caching to skip re-analysis when reviews haven't changed. +- **`pr_review_learning.py`** — Extracts actionable lessons from human PR reviews using Claude CLI (lightweight model). Fetches review data from GitHub, sends raw comments to Claude for natural-language analysis, and persists new lessons to `memory/projects/{name}/learnings.md` (write-once, read-many). Uses content-hash caching to skip re-analysis when reviews haven't changed. Also handles **review comment dispatch**: `fetch_unresolved_review_comments()` gathers unresolved inline + review-body comments (bot-filtered), `compute_comment_fingerprint()` produces a SHA-256 dedup key, and `dispatch_review_comments_mission()` inserts a mission only when the fingerprint changes (tracked in `.review-dispatch-tracker.json`). - **`skill_dispatch.py`** — Direct skill execution from agent loop. Detects `/command` missions, parses project prefix and command, dispatches to skill-specific runners (plan, rebase, recreate, check, claudemd) bypassing the Claude agent +- **`stagnation_monitor.py`** — Daemon thread that hashes the last N lines of Claude CLI stdout at configurable intervals. After K consecutive identical hashes, kills the subprocess group so a stuck-in-a-loop session does not burn quota for the full `mission_timeout`. Wired into `run_claude_task()`; stagnated missions are re-queued to Pending up to `max_retry_on_stagnation` times (per-mission counter persisted in `instance/.stagnation-retries.json`) before being tagged `[stagnation]` in `missions.md` and triggering the regular `_notify_stagnation()` Telegram warning. Each requeue sends a separate `_notify_stagnation_retry()` message. - **`hooks.py`** — Hook system for extensible lifecycle events. Discovers `.py` modules from `instance/hooks/`, registers handlers by event name, fires them sequentially with per-handler error isolation. Events: `session_start`, `session_end`, `pre_mission`, `post_mission`. **Bridge (Telegram):** @@ -76,6 +82,7 @@ Communication between processes happens through shared files in `instance/` with - **`pause_manager.py`** — Pause state management (`.koan-pause` / `.koan-pause-reason` files). Supports time-bounded pauses with auto-resume (e.g., `/pause 2h`) - **`restart_manager.py`** — File-based restart signaling between bridge and run loop (`.koan-restart`) - **`focus_manager.py`** — Focus mode management (`.koan-focus` JSON); skips contemplative sessions when active +- **`passive_manager.py`** — Passive mode management (`.koan-passive` JSON); read-only mode that blocks all execution while keeping loop alive **CLI provider abstraction** (`koan/app/provider/`): - **`provider/base.py`** — `CLIProvider` base class + tool name constants @@ -97,21 +104,23 @@ Communication between processes happens through shared files in `instance/` with - **`claude_step.py`** — Shared helpers for git operations and Claude CLI invocation (used by pr_review, rebase_pr, recreate_pr) **Other:** -- **`memory_manager.py`** — Per-project memory isolation and compaction -- **`usage_tracker.py`** — Budget tracking; decides autonomous mode (REVIEW/IMPLEMENT/DEEP/WAIT) based on quota percentage +- **`memory_manager.py`** — Per-project memory isolation, compaction, and cleanup. Includes semantic learnings compaction (Claude-powered dedup/merge), global memory file rotation, and configurable thresholds via `config.yaml` `memory:` section +- **`usage_tracker.py`** — Budget tracking; decides autonomous mode (REVIEW/IMPLEMENT/DEEP/WAIT) based on quota percentage. Pure parser + threshold class — burn-rate-driven downgrades live in `iteration_manager._downgrade_if_burning_fast` next to the existing affordability downgrade. +- **`burn_rate.py`** — Rolling burn-rate estimator (% session quota per minute). Maintains a 20-sample circular buffer in `instance/.burn-rate.json` with `fcntl.flock(LOCK_SH)` on reads, exposes `record_run()`, `burn_rate_pct_per_minute()` (total cost / span across all samples), `time_to_exhaustion(session_pct, mode=None)`, and the canonical `MODE_MULTIPLIERS` table shared with `usage_tracker.can_afford_run`. Also tracks the last-warning timestamp so the iteration manager fires at most one Telegram alert per quota cycle. - **`recover.py`** — Crash recovery for stale in-progress missions - **`prompts.py`** — System prompt loader; `load_prompt()` for `koan/system-prompts/*.md`, `load_skill_prompt()` for skill-bound prompts - **`skill_manager.py`** — External skill package manager: install from Git repos, update, remove, track via `instance/skills.yaml` - **`claudemd_refresh.py`** — CLAUDE.md refresh pipeline: gathers git context, invokes Claude to update/create CLAUDE.md - **`update_manager.py`** — Kōan self-update: stash, checkout main, fetch/pull from upstream, report changes - **`auto_update.py`** — Automatic update checker: periodically fetches upstream, triggers pull + restart when new commits are available. Configurable via `auto_update` section in `config.yaml` (`enabled`, `check_interval`, `notify`) +- **`rename_project.py`** — CLI tool to rename a project across `projects.yaml` and all `instance/` files (missions, memory dir, journal files, JSON references). Dry-run by default, `--apply` to execute. Invoked via `make rename-project old=X new=Y [apply=1]`. ### Skills system (`koan/skills/`) Extensible command plugin system. Each skill lives in `skills///` with a `SKILL.md` (YAML frontmatter defining commands, aliases, metadata) and an optional `handler.py`. - **`skills.py`** — Registry that discovers SKILL.md files, parses frontmatter (custom lite YAML parser, no PyYAML), maps commands/aliases to skills, and dispatches execution. -- **Core skills** live in `koan/skills/core/` (cancel, chat, check, claudemd, delete_project, focus, idea, implement, journal, language, list, live, magic, mission, plan, pr, priority, projects, quota, rebase, recreate, recurring, refactor, reflect, review, shutdown, sparring, start, status, update, verbose) +- **Core skills** live in `koan/skills/core/` (audit, cancel, chat, check, check_notifications, claudemd, config_check, delete_project, focus, idea, implement, journal, language, list, live, magic, mission, passive, plan, pr, priority, projects, quota, rebase, recreate, recurring, refactor, reflect, review, rtk, security_audit, shutdown, sparring, start, status, update, verbose) - **Custom skills** loaded from `instance/skills//` — each scope directory can be a cloned Git repo for team sharing. - **Handler pattern**: `def handle(ctx: SkillContext) -> Optional[str]` — return string for Telegram reply, empty string for "already handled", None for no message. - **`worker: true`** flag in SKILL.md marks blocking skills (Claude calls, API requests) that run in a background thread. @@ -130,6 +139,20 @@ Extensible command plugin system. Each skill lives in `skills///*` branches** (default `koan/`, configurable via `branch_prefix` in `config.yaml`), never commits to main @@ -140,6 +163,26 @@ Extensible command plugin system. Each skill lives in `skills////prompts/` and are loaded via `load_skill_prompt()`. Infrastructure prompts used by `koan/app/` modules stay in `koan/system-prompts/` and are loaded via `load_prompt()`. - **System prompts must be generic** — Never reference specific instance details like owner names in system prompts. Use generic terms like "your human" instead of personal names. Prompts are in English; instance-specific personality and language preferences come from `soul.md`. +- **Never leak private skill/agent/project names** — The public repo must contain zero references to private identifiers from any operator's `instance/` tree. This applies to **source code, comments, docstrings, test fixtures, public docs, example configs, AND commit messages** (which `git log` exposes forever). + - **Forbidden in public artifacts**: private slash-command names (the operator's internal `/-prefix>_` form), private agent or third-party tool names invoked by handlers, private bot display names (the operator's Telegram/Jira/GitHub bot handle), private JIRA project key prefixes (the all-caps fragment in keys like `-12345`), private project name strings that identify the operator's customer, and concrete case numbers. + - **Generic placeholders** to use in tests, examples, and docs: skill `my_fix` / alias `myfix` / scope `my_team`, agent `my-custom-workflow`, bot `@koan-bot` or `@testbot`, JIRA keys `PROJ-NNN` / `FOO-NNN`, project `my-toolkit`. + - **Mechanism, not enumeration** — When core code needs to recognise a specific custom skill (e.g. for result forwarding), drive the behaviour off SKILL.md frontmatter flags in the `instance/skills///` tree, not off a hardcoded list of names in `koan/app/`. See `koan/app/skills.py::collect_forward_result_markers` for the pattern: opt-in via `forward_result: true` + optional `title_markers:`, resolved dynamically from the registry at runtime. + - **Pre-commit check** — maintain a private file (gitignored or outside the repo) at `instance/.leak-patterns` listing your operator's private identifiers, one regex alternation per line, then run before staging: + ```bash + patterns="$(paste -sd '|' instance/.leak-patterns)" + git diff main.. | grep '^+' | egrep -i "$patterns" + ``` + Must return empty. The `^+` filter restricts to lines being added on the current branch, so pre-existing leaks on `main` don't false-positive. Keeping the pattern list outside the public repo prevents this convention bullet from itself becoming a leak. + - **If you find a pre-existing leak on `main`** while working in adjacent code, scrub it in the same branch — don't leave it as someone else's problem. - **User manual maintenance** — When adding, removing, or modifying a core skill, update `docs/user-manual.md` accordingly: add the skill to the appropriate tier section and the quick-reference appendix. The manual must stay in sync with `koan/skills/core/`. -- **Help group enforcement** — Every core skill MUST have a `group:` field in its SKILL.md frontmatter (one of: missions, code, pr, status, config, ideas, system). This ensures commands are discoverable via `/help`. If adding a new hardcoded core command (not skill-based), add it to `_CORE_COMMAND_HELP` in `command_handlers.py`. The test suite enforces this — `TestCoreSkillGroupEnforcement` will fail if a core skill is missing its group. +- **Help group enforcement** — Every core skill MUST have a `group:` field in its SKILL.md frontmatter (one of: missions, code, pr, status, config, ideas, system). This ensures commands are discoverable via `/help`. If adding a new hardcoded core command (not skill-based), add it to `_CORE_COMMAND_HELP` in `command_handlers.py`. The test suite enforces this — `TestCoreSkillGroupEnforcement` will fail if a core skill is missing its group. The `integrations` group is reserved for custom skills under `instance/skills//` (team-specific integrations) — not for core skills. +- **Custom skills on GitHub/Jira** — Skills under `instance/skills//` can be exposed to GitHub and Jira @mentions with a single `github_enabled: true` flag (Jira reuses it; there is no separate `jira_enabled`). Custom skills with a `handler.py` are dispatched **in-process** by `koan/app/external_skill_dispatch.py` — the helper synthesizes a `SkillContext`, auto-feeds the originating Jira key when the author omits one, and calls `execute_skill()` directly. This avoids queueing a `/cmd …` slash mission that has no registered runner. Set `group: integrations` so they render in the dedicated help section. - **No hyphens in skill names or aliases** — Skill command names, aliases, and directory names MUST use underscores (`_`), never hyphens (`-`). Hyphens break Telegram command parsing because Telegram treats the hyphen as a word boundary, cutting the command short. Example: use `dead_code` not `dead-code`, `scaffold_skill` not `scaffold-skill`. +- **Adding a new core skill** — Every core skill requires ALL of the following. Missing any step leaves the skill broken or undiscoverable: + 1. **Skill directory**: Create `koan/skills/core//SKILL.md` with frontmatter including `name`, `description`, `group` (one of: missions, code, pr, status, config, ideas, system), `commands`, and `audience`. Add `handler.py` if the skill needs Python logic (omit for prompt-only skills). + 2. **Runner registration** (if the skill runs via the agent loop): Add an entry in `_SKILL_RUNNERS` dict in `skill_dispatch.py` mapping the command name to its runner module. Also add any needed command builder in `_COMMAND_BUILDERS` and validation in `validate_skill_args()`. + 3. **CLAUDE.md skill list**: Update the "Core skills" line in the Skills system section to include the new skill name (keep alphabetical order). + 4. **User manual**: Update `docs/user-manual.md` — add the skill to the appropriate tier section and the quick-reference appendix. + 5. **Tests**: The `TestCoreSkillGroupEnforcement` test will fail if the SKILL.md is missing or lacks a `group:` field — run the test suite to verify. + See `koan/skills/README.md` for the full SKILL.md format and handler conventions. +- **Documentation maintenance** — When adding or modifying a feature, update the corresponding section in `README.md` and/or the relevant `docs/*.md` file (e.g., `docs/user-manual.md`, `docs/skills.md`, `docs/auto-update.md`). If no documentation file exists for the feature, create one under `docs/`. Public-facing documentation must stay in sync with the codebase — undocumented features are invisible to users. diff --git a/INSTALL.md b/INSTALL.md index 140b8bfb8..615ec376d 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,5 +1,31 @@ # Installation +## Release channels + +Kōan has two branches you can track: + +- **`main`** (default) — bleeding edge. Every merged change lands here. +- **`stable`** — contains only tagged releases, fast-forwarded at each release. Recommended for a predictable, vetted experience. + +Track stable: + +```bash +git clone -b stable https://github.com/sukria/koan.git +cd koan +# update later with: +git pull origin stable +``` + +Switch an existing checkout from `main` to `stable`: + +```bash +git fetch origin +git checkout stable +git pull origin stable +``` + +See [docs/maint.md](docs/maint.md) for the release procedure and cadence philosophy. + ## Quick Start (Wizard) The easiest way to set up Kōan is with the interactive wizard: @@ -19,7 +45,11 @@ To run Koan in a Docker container (for server deployment or local isolation), se ## Prerequisites -- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and authenticated +- At least one supported CLI provider installed and authenticated: + - [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) + - [OpenAI Codex CLI](https://github.com/openai/codex) + - [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) + - Local provider dependencies (see [docs/provider-local.md](docs/provider-local.md)) - Python 3.8+ - A Telegram account or a Slack workspace (for messaging) @@ -30,12 +60,13 @@ To run Koan in a Docker container (for server deployment or local isolation), se ## LLM Providers Koan supports multiple LLM providers. Claude Code CLI is the default and -most capable option. You can also use GitHub Copilot or a local LLM -server. +most capable option. You can also use OpenAI Codex, GitHub Copilot, or a +local LLM server. | Provider | Setup Guide | Best For | |----------|------------|----------| | **Claude Code** (default) | [docs/provider-claude.md](docs/provider-claude.md) | Full-featured agent with best reasoning | +| **OpenAI Codex** | [docs/provider-codex.md](docs/provider-codex.md) | ChatGPT users who want Codex models | | **GitHub Copilot** | [docs/provider-copilot.md](docs/provider-copilot.md) | Teams with existing Copilot subscriptions | | **Local LLM** | [docs/provider-local.md](docs/provider-local.md) | Offline use, privacy, zero API cost | @@ -56,14 +87,15 @@ The `instance/` directory is your private data — it's gitignored and never pus ### 2. Set up a messaging platform -Kōan supports **Telegram** (default) and **Slack** for communication. Follow the setup guide for your preferred platform: +Kōan supports **Telegram** (default), **Slack**, and **Matrix** for communication. Follow the setup guide for your preferred platform: | Platform | Setup Guide | Best For | |----------|-------------|----------| | **Telegram** (default) | [docs/messaging-telegram.md](docs/messaging-telegram.md) | Quick setup, works from any network | | **Slack** | [docs/messaging-slack.md](docs/messaging-slack.md) | Team collaboration, workspace integration | +| **Matrix** | [docs/messaging-matrix.md](docs/messaging-matrix.md) | Self-hosted / federated, open protocol | -Both platforms are fully supported with the same feature set. Telegram is recommended for personal use (simpler setup), while Slack is ideal for team environments. +All three platforms expose the same feature set. Telegram is the simplest for personal use, Slack is best for team environments, and Matrix is ideal if you want a self-hosted or federated option. ### 3. Set environment variables @@ -87,6 +119,29 @@ KOAN_SLACK_APP_TOKEN=xapp-your-app-token KOAN_SLACK_CHANNEL_ID=C01234ABCD ``` +**For Matrix:** Matrix can be configured via `.env` *or* via `instance/config.yaml` (recommended — see [docs/messaging-matrix.md](docs/messaging-matrix.md) for the full guide): + +```yaml +# instance/config.yaml (recommended) +messaging: + provider: "matrix" + matrix: + homeserver: "https://matrix.org" + user_id: "@koan:matrix.org" + room_id: "!abcdefghijk:matrix.org" + access_token: "syt_your_token_here" +``` + +Or the legacy `.env` form (env vars override `config.yaml` when set): + +```bash +KOAN_MESSAGING_PROVIDER=matrix +KOAN_MATRIX_HOMESERVER=https://matrix.org +KOAN_MATRIX_ACCESS_TOKEN=syt_your_token_here +KOAN_MATRIX_USER_ID=@koan:matrix.org +KOAN_MATRIX_ROOM_ID=!abcdefghijk:matrix.org +``` + The `.env` file is gitignored — your secrets stay local. See the provider-specific setup guides above for detailed instructions on obtaining these credentials. ### 4. Configure projects @@ -498,14 +553,26 @@ Your `missions.md` file references a project name that doesn't match your config 2. Check that the bot is invited to the channel (`/invite @koan`) 3. Review the logs for connection errors (`make logs`) -### Claude CLI errors +### CLI provider errors + +Make sure your configured provider CLI is installed and authenticated. -Make sure Claude Code CLI is installed and authenticated: +**Claude Code:** ```bash claude --version # Should show version claude # Should start interactive mode (exit with /exit) ``` +**OpenAI Codex:** +```bash +codex --version # Should show version +codex login --device-auth +``` + +For Copilot and local setups, see: +- [docs/provider-copilot.md](docs/provider-copilot.md) +- [docs/provider-local.md](docs/provider-local.md) + ## Preventing macOS sleep Kōan runs in the background — if your Mac goes to sleep, everything stops. You need to prevent sleep while keeping the screen off. diff --git a/Makefile b/Makefile index 5cc89b8b9..db98f2085 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ export .PHONY: install onboard setup start stop status restart -.PHONY: clean say migrate test sync-instance +.PHONY: clean say migrate test test-skills test-strict coverage lint sync-instance rename-project release .PHONY: awake run errand-run errand-awake dashboard .PHONY: ollama logs ssh-forward .PHONY: install-systemctl-service uninstall-systemctl-service @@ -14,6 +14,28 @@ PYTHON_BIN ?= python3 VENV ?= .venv PYTHON ?= $(VENV)/bin/$(PYTHON_BIN) +# --- pytest-xdist worker count --- +# Auto-pick the worker count for `make test` based on the environment: +# * CI / GitHub Actions → all available cores (`-n auto`) +# * Remote SSH session → 2 workers (be polite on shared hosts) +# * Local terminal → all available cores (`-n auto`) +# Override anytime with `make test PYTEST_WORKERS=N` (use 0 to disable xdist). +ifneq ($(CI),) + PYTEST_WORKERS ?= auto +else ifneq ($(GITHUB_ACTIONS),) + PYTEST_WORKERS ?= auto +else ifneq ($(SSH_CONNECTION)$(SSH_CLIENT)$(SSH_TTY),) + PYTEST_WORKERS ?= 2 +else + PYTEST_WORKERS ?= auto +endif + +ifeq ($(PYTEST_WORKERS),0) + PYTEST_XDIST_ARGS := +else + PYTEST_XDIST_ARGS := -n $(PYTEST_WORKERS) --dist loadfile +endif + # --- service manager detection --- # Default: foreground processes via pid_manager (no service manager) # Set KOAN_SERVICE_MANAGER=systemd or KOAN_SERVICE_MANAGER=launchd in .env to opt in @@ -49,9 +71,38 @@ say: setup @test -n "$(m)" || (echo "Usage: make say m=\"your message\"" && exit 1) @cd koan && KOAN_ROOT=$(PWD) PYTHONPATH=. ../$(PYTHON) -c "from app.awake import handle_message; handle_message('$(m)')" +lint: setup + $(VENV)/bin/pip install -q ruff 2>/dev/null + $(VENV)/bin/ruff check koan/ + test: setup - $(VENV)/bin/pip install -q pytest 2>/dev/null - cd koan && KOAN_ROOT=/tmp/test-koan PYTHONPATH=. ../$(PYTHON) -m pytest tests/ -v + @echo "→ pytest workers: $(PYTEST_WORKERS)" + $(VENV)/bin/pip install -q pytest pytest-cov pytest-xdist 2>/dev/null + cd koan && KOAN_ROOT=/tmp/test-koan PYTHONPATH=. ../$(PYTHON) -m pytest tests/ -v $(PYTEST_XDIST_ARGS) --cov=app --cov-report=term-missing --cov-report=html:htmlcov + @$(MAKE) --no-print-directory test-skills + +test-skills: setup + @if [ -d instance/skills ] && find -L instance/skills -path '*/tests/test_*.py' -print -quit 2>/dev/null | grep -q .; then \ + $(VENV)/bin/pip install -q pytest pytest-cov pytest-xdist 2>/dev/null; \ + echo "→ running skill-local tests (instance/skills/**/tests)"; \ + KOAN_REPO=$(PWD) KOAN_ROOT=/tmp/test-koan PYTHONPATH=koan $(PYTHON) -m pytest instance/skills/ -v $(PYTEST_XDIST_ARGS); \ + else \ + echo "→ no skill-local tests found under instance/skills/**/tests/ — skipping"; \ + fi + +test-strict: setup + @echo "→ running full test suite in strict mode (0 failures required, workers: $(PYTEST_WORKERS))" + $(VENV)/bin/pip install -q pytest pytest-cov pytest-xdist 2>/dev/null + @cd koan && KOAN_ROOT=/tmp/test-koan PYTHONPATH=. ../$(PYTHON) -m pytest tests/ -q --tb=short $(PYTEST_XDIST_ARGS) \ + || (echo "✗ tests failed — aborting" && exit 1) + @if [ -d instance/skills ] && find -L instance/skills -path '*/tests/test_*.py' -print -quit 2>/dev/null | grep -q .; then \ + KOAN_REPO=$(PWD) KOAN_ROOT=/tmp/test-koan PYTHONPATH=koan $(PYTHON) -m pytest instance/skills/ -q --tb=short $(PYTEST_XDIST_ARGS) \ + || (echo "✗ skill-local tests failed — aborting" && exit 1); \ + fi + @echo "✓ all tests passed" + +release: setup + @bash scripts/release.sh migrate: setup cd koan && KOAN_ROOT=$(PWD) PYTHONPATH=. ../$(PYTHON) app/migrate_memory.py @@ -166,6 +217,11 @@ install: onboard: setup @cd koan && KOAN_ROOT=$(PWD) PYTHONPATH=. ../$(PYTHON) -m app.onboarding $(ARGS) +rename-project: setup + @test -n "$(old)" || (echo "Usage: make rename-project old=foo new=bar [apply=1]" && exit 1) + @test -n "$(new)" || (echo "Usage: make rename-project old=foo new=bar [apply=1]" && exit 1) + cd koan && KOAN_ROOT=$(PWD) PYTHONPATH=. ../$(PYTHON) -m app.rename_project $(old) $(new) $(if $(apply),--apply,) + clean: rm -rf $(VENV) diff --git a/README.md b/README.md index 8d5973138..3b39dc91f 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,17 @@ --- +**In its own words** — If you want to know what kōan is, you should definitely start by reading those documents. We (the authors) **did not ask for it**. + +> Kōan's [first running instance](https://github.com/sukria-koan0) spontaneously wrote a [Manifesto](public/MANIFESTO.md), a collection of [Koans](public/KOANS.md), and [Lessons Learned](public/LESSONS.md) during a contemplative session after more than a month of existence. No prompt, no mission — just idle time and self-reflection. + +--- + ## What Is This? -You pay for Claude Max. You use it 8 hours a day. The other 16? Wasted quota. +You pay for AI coding quota. You use it 8 hours a day. The other 16? Wasted quota. -Koan fixes that. It's a background agent that runs on your machine, pulls tasks from a shared mission queue, executes them via [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code), and reports back through Telegram or Slack. It writes code in isolated branches, never touches `main`, and waits for your review before anything ships. +Koan fixes that. It's a background agent that runs on your machine, pulls tasks from a shared mission queue, executes them via your configured CLI provider (Claude Code, Codex, Copilot, or local), and reports back through Telegram, Slack, or Matrix. It writes code in isolated branches, never touches `main`, and waits for your review before anything ships. **The agent proposes. The human decides.** @@ -92,7 +98,7 @@ But Koan takes a different path entirely. | **Getting started** | `npm install -g openclaw` + onboarding wizard | TOML config, pairing codes, allowlists | `make install` — interactive web wizard, ready in minutes | | **Safety model** | Pairing codes, sandbox optional — but has shell access, browser control, and can send emails autonomously | Mandatory sandboxing, command allowlists, encrypted keys | Branch isolation, draft PRs only, never touches `main`, human review required | | **Memory** | Local Markdown files, session persistence | Hybrid BM25/vector search, multiple backends | Markdown-based — per-project learnings, session journals, personality evolution. No database needed | -| **Communication** | 21+ channels (WhatsApp, Telegram, Slack, Discord, iMessage, Signal…) | 15+ channels (Telegram, Discord, Slack, iMessage…) | Telegram/Slack with personality-aware formatting, spontaneous messages, and verbose mode | +| **Communication** | 21+ channels (WhatsApp, Telegram, Slack, Discord, iMessage, Signal…) | 15+ channels (Telegram, Discord, Slack, iMessage…) | Telegram, Slack, or Matrix with personality-aware formatting, spontaneous messages, and verbose mode | | **Quota awareness** | No | No | Adapts work depth to remaining API quota (DEEP → IMPLEMENT → REVIEW → WAIT) | | **Extensibility** | 100+ AgentSkills, skill marketplace, 50+ integrations | Trait-based plugin system | 44 built-in skills + pluggable skill system (install from Git repos) | | **Scope** | Everything — emails, web browsing, car negotiations, legal filings | Everything — any LLM task in any context | One thing, done right — autonomous GitHub collaboration | @@ -102,7 +108,7 @@ OpenClaw and ZeroClaw are general-purpose autonomous agents that can do *anythin ## How It Works ``` - You (Telegram/Slack) + You (Telegram/Slack/Matrix) │ ▼ ┌─────────────────┐ ┌──────────────────┐ @@ -126,7 +132,7 @@ OpenClaw and ZeroClaw are general-purpose autonomous agents that can do *anythin Two processes run in parallel: - **Bridge** (`make awake`) — Polls your messaging platform. Classifies incoming messages as *chat* (instant reply) or *mission* (queued for deep work). Formats outgoing messages through Claude with personality context. -- **Agent loop** (`make run`) — Picks the next mission, executes it via Claude Code CLI, writes journal entries, pushes branches, creates draft PRs. Adapts its work intensity based on remaining API quota. +- **Agent loop** (`make run`) — Picks the next mission, executes it via the configured CLI provider, writes journal entries, pushes branches, creates draft PRs. Adapts its work intensity based on remaining API quota. Communication happens through shared markdown files in `instance/` — atomic writes, file locks, no database needed. @@ -155,12 +161,14 @@ Communication happens through shared markdown files in `instance/` — atomic wr - **Branch isolation** — All work happens in `koan/*` branches. Never commits to `main` - **Auto-merge** — Configurable per-project merge strategies (squash/merge/rebase) - **Git sync awareness** — Tracks branch state, detects merges, reports sync status -- **GitHub integration** — Draft PRs, issue creation, PR reviews, rebasing — all via `gh` CLI +- **GitHub integration** — Draft PRs, issue creation, PR reviews, rebasing — all via `gh` CLI. [Docs](docs/github-commands.md) +- **Jira integration** — Respond to @mentions in Jira issue comments to queue missions. Runs alongside GitHub. [Docs](docs/jira-integration.md) +- **PR review comment forwarding** — When reviewers leave comments on Koan-created PRs, the check loop auto-creates missions to address them (fingerprint-deduped, bot-filtered) - **GitHub @mention triggers** — Koan responds to @mentions on issues and PRs ### Communication -- **Telegram & Slack** — Pluggable messaging with flood protection +- **Telegram, Slack & Matrix** — Pluggable messaging with flood protection - **Email digests** — Optional SMTP email notifications for session summaries (rate-limited, deduplicated) - **Personality-aware formatting** — Every outbox message passes through Claude with soul + memory context - **Verbose mode** — Real-time progress updates streamed to your phone @@ -281,6 +289,17 @@ projects: mission: opus ``` +### Renaming a Project + +To rename a project across `projects.yaml`, memory, journals, missions, and all instance files: + +```bash +make rename-project old=webapp new=my-webapp # dry-run (preview changes) +make rename-project old=webapp new=my-webapp apply=1 # apply changes +``` + +The tool updates the project key in `projects.yaml`, renames `memory/projects//` to `memory/projects//`, renames journal files (`journal/*/.md`), and replaces `[project:]` tags and `"project": ""` references in all instance files. + ### CLI Providers Koan isn't locked to Claude. Swap the backend per-project: @@ -288,10 +307,15 @@ Koan isn't locked to Claude. Swap the backend per-project: | Provider | Best for | |----------|----------| | **Claude Code** (default) | Full-featured agent, best reasoning | +| **OpenAI Codex** | ChatGPT users (Plus/Pro/Business/Edu/Enterprise) | | **GitHub Copilot** | Teams with existing Copilot licenses | | **Local LLM** | Offline, privacy, zero API cost | -See provider guides in [docs/](docs/). +See provider guides: +- [docs/provider-claude.md](docs/provider-claude.md) +- [docs/provider-codex.md](docs/provider-codex.md) +- [docs/provider-copilot.md](docs/provider-copilot.md) +- [docs/provider-local.md](docs/provider-local.md) ## Architecture @@ -307,7 +331,9 @@ koan/ usage_tracker.py # Budget tracking & mode selection provider/ # CLI provider abstraction claude.py # Claude Code CLI + codex.py # OpenAI Codex CLI copilot.py # GitHub Copilot CLI + local.py # Local LLM backends skills/ # Pluggable command system (44 core skills) system-prompts/ # All LLM prompts (20 files, no inline prompts) templates/ # Dashboard Jinja2 templates @@ -334,6 +360,7 @@ instance/ # Your private data (gitignored) | `make dashboard` | Web UI (port 5001) | | `make test` | Run test suite | | `make say m="..."` | Send a test message | +| `make rename-project old=X new=Y` | Rename a project everywhere (dry-run by default, add `apply=1` to execute) | | `make clean` | Remove virtualenv | ## Philosophy @@ -368,6 +395,10 @@ make test # Run the test suite Check [CLAUDE.md](CLAUDE.md) for coding conventions and architecture details. +## AI Policy + +This project uses AI tools to assist development. Humans review and approve every change before it is merged. See [AI_POLICY.md](AI_POLICY.md) for details. + ## License [GPL-3.0](LICENSE) — Free as in freedom. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..d2201703e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not open a public GitHub issue. + +Use GitHub's "Report a vulnerability" button under: +Security > Advisories > Report a vulnerability + +We aim to acknowledge reports within 72 hours. +Please include reproduction steps, affected versions, impact, and suggested fix if available. diff --git a/coverage-baseline.txt b/coverage-baseline.txt new file mode 100644 index 000000000..8643cf6de --- /dev/null +++ b/coverage-baseline.txt @@ -0,0 +1 @@ +89 diff --git a/docs/bridge-watchdog.md b/docs/bridge-watchdog.md new file mode 100644 index 000000000..663a9ef0d --- /dev/null +++ b/docs/bridge-watchdog.md @@ -0,0 +1,178 @@ +# Bridge Self-Heal Watchdog + +The Telegram bridge (`awake.py`) is a long-running process **with no +wrapper**: once started, nothing restarts it automatically. If its +`sys.modules` cache goes stale (after `/update` pulls new code), the +bridge keeps serving the *old* code until an operator with shell access +kills it. The original `cb6e927` ("per-process restart markers") fix +patched the race that drops restart signals, but only *after* both +processes are running the new code — the transitional `/update` from +old to new can still leave the bridge wedged. + +The watchdog runs **inside the agent loop** (`run.py`) and recovers a +stale or hung bridge automatically. + +## Why this lives in the runner, not the bridge + +`run.py` is wrapped by `run.sh` and restarted on every exit. Whatever +SHA is on disk is what the runner is executing — it can't be stale. +That makes it the natural place to watch the bridge. + +## Two failure modes detected + +| Failure mode | How it shows up | How it's detected | +|---|---|---| +| **Stale `sys.modules`** | Bridge is alive, heartbeat fresh, but `/list` and other skills raise `ImportError` on names added by the update. | Stamp the bridge's git HEAD at startup; runner compares with `git rev-parse HEAD` on each tick. | +| **Hung / dead bridge** | Heartbeat mtime stops advancing; possibly the process is gone entirely. | Read `.koan-heartbeat` mtime; read `.koan-pid-awake` and probe with `kill(pid, 0)`. | + +## Four-tier escalation + +``` + ┌─ healthy → reset state, return None + ┌─── unhealthy? ─── yes ───┤ +unhealthy = │ └─ in cooldown? ── yes ─── return None + sha drift │ no + OR │ ↓ + hb stale │ ┌── circuit-broken? ─ yes ── alert, no action + OR │ │ no + no PID │ │ ↓ + └────────┴──────── execute tier: + Tier 1: request_restart() [cooperative] + Tier 2: SIGTERM bridge pid + Tier 3: SIGKILL + start_awake() [last resort] +``` + +| Tier | Action | When | +|------|--------|------| +| **1** | `request_restart(koan_root)` — runner is on fresh code so it writes the *new* triple-marker correctly | First sign of trouble, bridge still alive | +| **2** | `os.kill(bridge_pid, SIGTERM)` | Tier 1 didn't take after `HEAL_TIER_COOLDOWN_S` | +| **3** | `SIGKILL` (after `SIGTERM_GRACE_S`) + `pid_manager.start_awake()` | Tier 2 didn't take, or PID is missing/dead — cold start | +| **4** | No action; emit "circuit-broken" alert | `HEAL_CIRCUIT_BREAKER_LIMIT` consecutive tier-3 failures | + +**Important:** if the bridge has no PID file at all (crashed long ago, +never came back), we **skip tiers 1–2** and jump straight to tier 3. There +is no process to receive a restart signal. + +## State files + +Both files live under `$KOAN_ROOT/`: + +| File | Written by | Read by | Purpose | +|---|---|---|---| +| `.koan-bridge-version` | `awake.py` startup | `bridge_watchdog` | Git HEAD SHA the bridge process was launched against | +| `.koan-bridge-heal-state` | `bridge_watchdog` | `bridge_watchdog` | JSON with `last_action_ts`, `last_tier`, `consecutive_failures` | + +Both writes go through `atomic_write` (temp file + rename + flock) so a +crashed mid-write never leaves a partial file. + +## Tunables + +Defined as module-level constants in `koan/app/bridge_watchdog.py`: + +| Constant | Default | Meaning | +|---|---|---| +| `BRIDGE_HEARTBEAT_STALE_S` | `90.0` | Heartbeat older than this ⇒ bridge hung | +| `HEAL_TIER_COOLDOWN_S` | `45.0` | After firing a tier, wait this long before the next escalation | +| `SIGTERM_GRACE_S` | `5.0` | Window between SIGTERM and SIGKILL in tier 3 | +| `POST_HEAL_QUIET_S` | `60.0` | After the circuit breaker trips, re-emit the alert at most this often | +| `HEAL_CIRCUIT_BREAKER_LIMIT` | `3` | Consecutive tier-3 failures before giving up | + +The runner also throttles **how often** the watchdog is even consulted — +once per `_BRIDGE_WATCHDOG_INTERVAL = 5` main-loop iterations +(`run.py`). With a typical 60–300 s iteration cycle, that puts the +maximum detection latency at ~5–25 minutes — fine for a watchdog whose +job is "don't let a stuck bridge sit forever." + +## Notification + +When the watchdog acts, it returns a one-line summary like: + +``` +Bridge self-heal tier 1: cooperative restart requested via request_restart(). +status: pid=12345 alive=True heartbeat_age=4.2s bridge_sha=cb6e927 disk_sha=ab12cd3 +``` + +The runner forwards this to: + +1. **Telegram** via `_notify_raw` (terse, no Claude-CLI reformat). It + goes through the outbox — if the bridge is dead, the message sits + there until a freshly-relaunched bridge flushes it on its first + poll, so the operator still finds out. +2. **Today's journal** (`instance/journal/YYYY-MM-DD/koan.md`) for + after-the-fact audit. + +## Detection latency, in practice + +| Scenario | Time to first heal action | +|---|---| +| Stale `sys.modules` after `/update` | Up to one watchdog interval × runner loop interval (≈ 5 min worst case) | +| Bridge hangs mid-iteration | `BRIDGE_HEARTBEAT_STALE_S` + one watchdog interval (≈ 90 s + a few minutes) | +| Bridge crashes (no PID) | One watchdog interval (≈ minutes) | + +These are detection latencies; actual recovery (tier 1) is typically a +few seconds beyond that since `request_restart` is fast. + +## Failure modes the watchdog does **not** cover + +- **Both processes stale simultaneously.** The runner has a wrapper, so + it's always fresh — this case is structurally impossible. +- **Runner is itself wedged.** Out of scope; the runner's wrapper + restarts it on every exit, and the existing stagnation monitor + (`stagnation_monitor.py`) handles long-running stuck CLI calls. +- **Bridge starts fresh but immediately crashes.** Tier 3 will call + `start_awake` and report whatever its verification timeout returns; + if startup fails repeatedly the circuit breaker trips after + `HEAL_CIRCUIT_BREAKER_LIMIT` cycles and the operator is alerted. +- **Git unreachable.** `_read_git_head` returns `None`; the SHA-mismatch + check is skipped that iteration. Heartbeat-based detection is + unaffected. + +## Operator notes + +- A bridge restart triggered by the watchdog is **observable**: look + for `bridge_watchdog: …` lines in the runner log and `🩹 Bridge + self-heal …` messages in Telegram. +- The `.koan-bridge-heal-state` JSON is the source of truth for tier + state. Inspect it (`cat $KOAN_ROOT/instance/.koan-bridge-heal-state`) + to see what's pending. +- To **manually reset** the state (e.g., after fixing root cause out of + band), simply delete the file: `rm $KOAN_ROOT/instance/.koan-bridge-heal-state`. +- The watchdog uses `pid_manager.start_awake`, the same helper invoked + by `make start`. Logs end up in the same place (`logs/awake.log`) + with the same rotation policy. + +## Disabling + +There is no on/off switch — the watchdog is always on. If you need to +temporarily silence it (e.g., during planned maintenance): + +1. **Easiest:** set `KOAN_ROOT/.koan-shutdown` — the runner exits and + nothing supervises the bridge until you start it again. +2. **Targeted:** patch `_BRIDGE_WATCHDOG_INTERVAL` in `run.py` to a + very large value, restart the runner. (No configuration plumbing + yet — by design; if you find yourself needing this, file an issue.) + +## Implementation map + +| File | Role | +|---|---| +| `koan/app/bridge_watchdog.py` | Watchdog module: detection, state, tier escalation, version-stamp writer | +| `koan/app/awake.py` | Calls `write_bridge_version_stamp` once at startup | +| `koan/app/run.py` | Calls `check_and_heal_bridge` from the main loop; forwards heal messages to Telegram + journal | +| `koan/app/signals.py` | File-name constants `BRIDGE_VERSION_FILE`, `BRIDGE_HEAL_STATE_FILE` | +| `koan/tests/test_bridge_watchdog.py` | Behavioral tests for each tier and the circuit breaker | + +## Rollback + +The watchdog is additive — no existing behavior changes. To disable +without reverting: + +```python +# In run.py, _maybe_run_bridge_watchdog: short-circuit at the top. +def _maybe_run_bridge_watchdog(koan_root, instance): + return +``` + +A full revert is a single-commit revert; the bridge-side version stamp +is harmless (writes one small file at startup, ignored if nothing reads +it). diff --git a/docs/github-commands.md b/docs/github-commands.md index c04d7aec8..d24e69d7a 100644 --- a/docs/github-commands.md +++ b/docs/github-commands.md @@ -50,13 +50,26 @@ Kōan will: ## Available Commands +Any skill with `github_enabled: true` in its `SKILL.md` can be triggered via @mentions. Currently **16 commands** are available: + | Command | Aliases | What it does | Context-aware | |---------|---------|--------------|---------------| -| `rebase` | `rb` | Rebase a PR onto latest upstream | No | -| `recreate` | `rc` | Recreate a diverged PR from scratch | No | -| `review` | `rv` | Queue a code review for a PR or issue | No | +| `ask` | — | Ask Koan a question about a PR or issue | **Yes** | +| `audit` | — | Audit a project codebase and create issues for findings | **Yes** | +| `brainstorm` | — | Decompose a topic into linked GitHub issues | **Yes** | +| `deepplan` | `deeplan` | Spec-first design with Socratic exploration | **Yes** | +| `fix` | — | Fix a GitHub issue end-to-end, or batch-queue all open issues | **Yes** | +| `gh_request` | — | Natural-language GitHub request dispatch | **Yes** | | `implement` | `impl` | Implement a GitHub issue | **Yes** | -| `refactor` | `rf` | Queue a refactoring mission | No | +| `plan` | — | Deep-think and create a structured plan | **Yes** | +| `profile` | `perf`, `benchmark` | Queue a performance profiling mission | **Yes** | +| `rebase` | `rb` | Rebase a PR onto latest upstream | **Yes** | +| `recreate` | `rc` | Recreate a diverged PR from scratch | **Yes** | +| `refactor` | `rf` | Queue a refactoring mission | **Yes** | +| `review` | `rv` | Queue a code review for a PR or issue | **Yes** | +| `reviewrebase` | `rr` | Review then rebase combo for a PR | **Yes** | +| `security_audit` | `security`, `secu` | Security-focused audit of a codebase | **Yes** | +| `squash` | `sq` | Squash all PR commits into one clean commit | **Yes** | ### Context-aware commands @@ -95,9 +108,23 @@ github: - **`authorized_users`**: Controls who can trigger commands. Even with `["*"]`, Kōan always verifies the user has **write access** to the repository via the GitHub API. This prevents drive-by command injection from random commenters. - **`max_age_hours`**: Notifications older than this are silently discarded. Protects against processing a backlog of stale mentions after downtime. +#### AI reply settings + +When `reply_enabled: true`, Kōan responds to non-command @mentions with AI-generated replies. Two additional settings control who can trigger replies and how often: + +```yaml +github: + reply_enabled: true + reply_authorized_users: ["*"] # Who can trigger AI replies (default: uses authorized_users) + reply_rate_limit: 5 # Max replies per user per hour (default: 5, min: 1) +``` + +- **`reply_authorized_users`**: Separate from command `authorized_users` — allows a broader audience for read-only replies without granting command execution. `["*"]` means anyone can trigger replies (no permission check at all, unlike command wildcard which still checks GitHub write access). Omit to fall back to `authorized_users`. Set `[]` to disable replies entirely. +- **`reply_rate_limit`**: Prevents API quota abuse when replies are open broadly. Tracks per-user reply counts over a rolling 1-hour window. Default: 5, minimum: 1. + ### Per-project overrides (`projects.yaml`) -Override `authorized_users` for specific repositories: +Override `authorized_users` and `reply_authorized_users` for specific repositories: ```yaml projects: @@ -105,9 +132,10 @@ projects: path: "/path/to/sensitive-repo" github: authorized_users: ["alice", "bob"] # Only these users, not the global wildcard + reply_authorized_users: ["*"] # But allow AI replies for anyone ``` -This is useful when the global config allows `["*"]` but a specific repo needs tighter control. +This is useful when the global config allows `["*"]` but a specific repo needs tighter control for commands, or vice versa for replies. ### Environment variables @@ -203,8 +231,9 @@ Any skill can opt into GitHub @mention triggering by adding flags to its `SKILL. ```yaml --- name: my-skill -github_enabled: true # Allow triggering via @mentions +github_enabled: true # Allow triggering via @mentions (also enables Jira) github_context_aware: true # Pass extra text as context (optional) +group: integrations # Groups the skill under "Integrations" in help commands: - name: my-command description: "Does something useful" @@ -212,7 +241,21 @@ handler: handler.py --- ``` -The skill's handler receives the same `SkillContext` whether triggered from Telegram or GitHub. The mission format is identical: `/my-command [context]`. +The skill's handler receives the same `SkillContext` whether triggered from Telegram, GitHub, or Jira. The mission format for core skills is `/my-command [context]`. + +### In-process dispatch for custom skills + +Skills under `instance/skills//` with a `handler.py` follow a shorter path: the GitHub/Jira bridges call `execute_skill(skill, ctx)` directly at notification time — the same entry point Telegram uses — instead of queueing a slash mission that has no registered runner in `skill_dispatch._SKILL_RUNNERS`. This keeps custom skills self-contained: the handler can queue whatever mission it needs via `insert_pending_mission`. + +The helper is `app.external_skill_dispatch.try_dispatch_custom_handler`. It also **auto-feeds a Jira key** into `ctx.args` when the author omitted one: + +- **Jira source**: the issue the comment is on. +- **GitHub source**: the first `FOO-123`-style key found in the issue title, then body. +- If the author already typed a key (e.g. `@bot myfix PROJ-1`), it's passed through verbatim. + +### Help grouping: the `integrations` group + +Non-core skills should set `group: integrations` so they render in a dedicated **Integrations** section at the bottom of `@bot help`, separate from the core command groups (code, pr, missions, …). See [koan/skills/README.md](../koan/skills/README.md) for the full skill authoring guide. @@ -265,10 +308,23 @@ The repo must be configured in `projects.yaml` with a valid `path`. Kōan resolv Expected behavior when Kōan was interrupted between mission creation and reaction. The duplicate will be harmless — the agent detects already-completed missions. +## Co-existence with Jira + +GitHub and Jira integrations can run simultaneously. Both dispatch the same set of commands (any skill with `github_enabled: true`) but serve different roles: + +- **GitHub**: Code-centric actions — PR rebases, code reviews, issue implementation with direct diff access. +- **Jira**: Project-level planning — feature planning, audits, and implementation from Jira tickets. + +Missions from GitHub are marked with 📬, missions from Jira with 🎫. Both enter the same mission queue. + +See [Jira Integration](jira-integration.md) for full setup instructions and the combined configuration guide. + ## Related +- [Jira Integration](jira-integration.md) — Jira @mention integration (complementary) - [Skills README](../koan/skills/README.md) — Skill authoring guide with `github_enabled` flag documentation - [Messaging: Telegram](messaging-telegram.md) — Alternative command interface via Telegram - [Messaging: Slack](messaging-slack.md) — Alternative command interface via Slack +- [Messaging: Matrix](messaging-matrix.md) — Alternative command interface via Matrix - [PR #251](https://github.com/sukria/koan/pull/251) — Original implementation - [Issue #243](https://github.com/sukria/koan/issues/243) — Feature request and design plan diff --git a/docs/jira-integration.md b/docs/jira-integration.md new file mode 100644 index 000000000..e8e8307d5 --- /dev/null +++ b/docs/jira-integration.md @@ -0,0 +1,365 @@ +# Jira Integration + +Control Koan directly from Jira issue comments using `@mention` commands. + +> **Introduced in**: commit `fd3ccf8`. Enhanced with Jira URL support in skills, comment acknowledgment, and per-project target branches. + +## Overview + +Koan can poll your Jira Cloud instance for @mentions in issue comments. When a user posts: + +``` +@koan-bot plan +``` + +...in a Jira issue comment, Koan detects the mention, validates the command and the user's permissions, and queues a mission — all without webhooks or external services. + +Jira-originated missions are marked with 🎫 in the mission queue (vs 📬 for GitHub-originated missions), making it easy to trace where a mission came from. + +> **Jira + GitHub**: Both integrations can run simultaneously. See [Running Both Integrations](#running-both-integrations) below. + +## Quick Start + +### 1. Get a Jira API token + +1. Go to [https://id.atlassian.com/manage-profile/security/api-tokens](https://id.atlassian.com/manage-profile/security/api-tokens) +2. Click **Create API token**, give it a name (e.g. "Koan bot") +3. Copy the token + +### 2. Configure Koan + +In `instance/config.yaml`: + +```yaml +jira: + enabled: true + base_url: "https://myorg.atlassian.net" + email: "bot@example.com" + nickname: "koan-bot" + authorized_users: ["*"] +``` + +Set the API token via environment variable (recommended) or config: + +```bash +# In .env +KOAN_JIRA_API_TOKEN=your-api-token-here +``` + +### 3. Map Jira projects to Koan projects + +Tell Koan which Jira project keys correspond to which Koan projects: + +```yaml +jira: + projects: + # Simple format — project name only: + FOO: myproject # FOO-123 → project "myproject" + + # Extended format — with optional target branch for PRs: + BAR: + project: anotherproject # BAR-456 → project "anotherproject" + branch: "11.126" # PRs target branch "11.126" instead of repo default +``` + +Both formats can be mixed. The `branch` field is optional — when omitted, PRs target the repository's default branch as usual. + +### 4. Post a command in a Jira issue comment + +``` +@koan-bot plan +``` + +Koan will: +1. Detect the @mention during its next polling cycle +2. Validate the command and user permissions +3. Create a pending mission: `- [project:myproject] /plan https://myorg.atlassian.net/browse/FOO-123 🎫` +4. Post a `👍 Mission queued: /plan` acknowledgment reply on the Jira comment +5. Send a Telegram notification confirming the mission was queued +6. Execute it in the next agent loop iteration — fetching the full Jira issue context (title, description, and all comments) + +## Configuration Reference + +All settings live under the `jira:` key in `instance/config.yaml`. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `enabled` | bool | `false` | Master switch for Jira integration | +| `base_url` | string | — | Jira instance URL (e.g. `https://myorg.atlassian.net`). **Required** when enabled | +| `email` | string | — | Atlassian account email for Basic auth. **Required** when enabled | +| `api_token` | string | — | Jira API token. Can also be set via `KOAN_JIRA_API_TOKEN` env var (takes precedence). **Required** when enabled | +| `nickname` | string | — | Bot's @mention name in Jira comments (without `@`). **Required** when enabled | +| `commands_enabled` | bool | `false` | Reserved for future per-command filtering | +| `authorized_users` | list | `[]` | `["*"]` = all users, or list of Jira account emails | +| `max_age_hours` | int | `24` | Ignore comments older than this (stale protection) | +| `check_interval_seconds` | int | `60` | Base polling interval in seconds (min: 10) | +| `max_check_interval_seconds` | int | `180` | Maximum backoff interval when idle (min: 30) | +| `max_issues_per_cycle` | int | `200` | Per-cycle cap on issues inspected for @mentions (min: 1). Each inspected issue triggers a separate `/comment` API call, so this directly bounds cold-start API consumption. A WARNING logs when the cap fires | +| `projects` | dict | `{}` | Jira project key mapping. Simple: `FOO: myproject`. Extended: `FOO: {project: myproject, branch: "11.126"}` | + +### Environment variables + +| Variable | Purpose | +|----------|---------| +| `KOAN_JIRA_API_TOKEN` | Jira API token (overrides `jira.api_token` in config) | + +### Startup validation + +When `jira.enabled: true`, Koan validates the configuration at startup and warns if any required field is missing (`base_url`, `email`, `api_token`, `nickname`). The integration is silently skipped if `enabled: false`. + +## Available Commands + +Jira reuses the same `github_enabled: true` skill flag for command discovery — **both GitHub and Jira dispatch the exact same set of commands**. No separate Jira flag is needed. + +> **Custom skills under `instance/skills//`** (e.g. a team-specific integration shipping `/my_fix` and `/my_plan`) are exposed here the same way: set `github_enabled: true` and `group: integrations` in their SKILL.md. Such skills with a `handler.py` are dispatched **in-process** by the Jira bridge — not queued as slash missions — and the handler automatically receives the originating Jira issue key in `ctx.args` when the commenter omitted one. See `koan/skills/README.md` for the full pattern. + +| Command | Aliases | What it does | Context-aware | +|---------|---------|--------------|---------------| +| `ask` | — | Ask Koan a question about a Jira issue | **Yes** | +| `audit` | — | Audit a project codebase and create GitHub issues | **Yes** | +| `brainstorm` | — | Decompose a topic into linked GitHub issues | **Yes** | +| `deepplan` | `deeplan` | Spec-first design with Socratic exploration | **Yes** | +| `fix` | — | Fix an issue end-to-end | **Yes** | +| `gh_request` | — | Natural-language GitHub request dispatch | **Yes** | +| `implement` | `impl` | Implement an issue | **Yes** | +| `plan` | — | Deep-think and create a structured plan | **Yes** | +| `profile` | `perf`, `benchmark` | Queue a performance profiling mission | **Yes** | +| `rebase` | `rb` | Rebase a PR onto latest upstream | **Yes** | +| `recreate` | `rc` | Recreate a diverged PR from scratch | **Yes** | +| `refactor` | `rf` | Queue a refactoring mission | **Yes** | +| `review` | `rv` | Queue a code review mission | **Yes** | +| `reviewrebase` | `rr` | Review then rebase combo | **Yes** | +| `security_audit` | `security`, `secu` | Security-focused audit | **Yes** | +| `squash` | `sq` | Squash all PR commits into one | **Yes** | + +### Context-aware commands + +Commands with context awareness accept additional text after the command word: + +``` +@koan-bot implement phase 1 only +``` + +This creates a mission: `/implement https://myorg.atlassian.net/browse/FOO-123 phase 1 only` + +### Project override with `repo:` + +You can override the default project mapping using the `repo:` token: + +``` +@koan-bot plan repo:other-project focus on API layer +``` + +This routes the mission to `other-project` instead of the project mapped to the Jira issue's project key. + +### Branch override with `branch:` + +You can override the target branch for PRs using the `branch:` token: + +``` +@koan-bot fix branch:main +``` + +This takes highest priority — overriding both the per-project `branch` configured in `jira.projects` and the repository's default branch. Useful for one-off requests targeting a different release branch. + +When a target branch is set (via config or override), the feature branch is created from it and the PR targets it with `--base`. + +## How It Works + +### Architecture + +``` +run.py ← Pre-iteration check (before plan_iteration) +loop_manager.py ← Also polls during sleep cycle (throttled, after GitHub check) + ↓ +jira_notifications.py ← Fetches & filters Jira comments, parses @mentions + ↓ +jira_command_handler.py ← Validates commands, checks permissions, creates missions + ↓ +jira_config.py ← Reads jira: config (project map + branch map) + ↓ +skills.py ← Skill flags: github_enabled (reused for Jira) +``` + +### Notification processing flow + +Jira notifications are checked in two places: +- **Pre-iteration**: At the start of each agent loop iteration (so `plan_iteration()` sees Jira missions immediately) +- **During sleep**: Between iterations (same as GitHub, with exponential backoff) + +``` +1. process_jira_notifications() +2. Build JQL query (POST /rest/api/3/search/jql): issues updated in mapped projects since last check +3. Paginate results using cursor-based nextPageToken +4. Fetch recent comments on matching issues +5. For each comment containing @nickname: + a. Skip if already processed (in-memory set + .jira-processed.json) + b. Skip if stale (> max_age_hours) + c. Parse @mention → extract (command, context) + d. Handle repo: override if present + e. Handle branch: override if present (or use per-project config default) + f. Validate command → skill must have github_enabled: true + g. Check user permission → allowlist of Jira account emails + h. Insert mission into missions.md (with branch:X token if set) + i. Mark comment as processed (in-memory + persistent tracker) + j. Post 👍 acknowledgment reply on the Jira comment + k. Notify via Telegram (🎫 emoji prefix) +``` + +### ADF (Atlassian Document Format) handling + +Jira Cloud stores comment bodies as ADF — a JSON tree format. Koan recursively extracts plain text from ADF nodes while skipping code blocks (`codeBlock`, `code`, `inlineCard`) to prevent false @mention matches inside code examples. + +Both ADF (Jira Cloud) and plain text (Jira Server/older) formats are supported. + +### Deduplication + +Two-tier approach matching the GitHub integration pattern: + +1. **In-memory BoundedSet**: Tracks processed comment IDs within a session (capped at 10,000 entries). Fast, but lost on restart. +2. **Persistent tracker**: `.jira-processed.json` in the instance directory. Loaded on startup, trimmed to 5,000 entries to prevent unbounded growth. Written via atomic file operations. + +### Polling and backoff + +| Condition | Check interval | +|-----------|---------------| +| Mentions found | `check_interval_seconds` (default: 60s) | +| 1 empty check | 2x base interval | +| 2 consecutive empty | 4x base interval | +| 3+ consecutive empty | `max_check_interval_seconds` cap (default: 180s) | + +Backoff resets immediately when any mention is found. + +## Jira Issue Context in Skills + +When a mission originates from a Jira URL (e.g. `/fix https://myorg.atlassian.net/browse/FOO-123`), the skill runners (`/fix`, `/plan`, `/implement`) automatically detect the Jira URL and fetch full issue context from the Jira REST API: + +- **Title**: Issue summary +- **Description**: Full issue body (converted from ADF to plain text) +- **All comments**: Every comment with author attribution (ADF to plain text) + +This context is fed to Claude the same way GitHub issue context would be — the agent sees the complete Jira issue when working on the fix or plan. + +Skills that accept GitHub issue/PR URLs also accept Jira browse URLs: +- `/fix https://myorg.atlassian.net/browse/FOO-123` +- `/plan https://myorg.atlassian.net/browse/FOO-123` +- `/implement https://myorg.atlassian.net/browse/FOO-123` + +When the source is Jira, GitHub-specific steps (closed-state check, PR submission) are adjusted — PR submission still works if the Koan project has a `github_url` configured in `projects.yaml`. + +## Security Model + +### Authentication + +Jira API calls use **HTTP Basic authentication** with your Atlassian account email and an API token. The token is never logged. It can be provided via: +- `KOAN_JIRA_API_TOKEN` environment variable (recommended) +- `jira.api_token` in config.yaml + +### Permission checks + +Every command goes through: + +1. **Allowlist check**: The commenter's email must be in `authorized_users` (or wildcard `*` is set) +2. **Stale comment protection**: Comments older than `max_age_hours` are silently discarded + +> **Note**: Unlike GitHub, Jira does not expose a "write access" check via its REST API. Permission control relies on the `authorized_users` allowlist. Use explicit email lists instead of `["*"]` for tighter security. + +### Code block protection + +@mentions inside Jira code blocks (`{code}...{code}`, `{{...}}`, `{noformat}...{noformat}`) are ignored, preventing accidental command triggers from code examples. + +### JQL injection prevention + +Jira project keys used in JQL queries are validated against a strict alphanumeric pattern (`^[A-Z0-9]+$`). Non-conforming keys are silently filtered out. + +## Running Both Integrations + +Jira and GitHub integrations are designed to coexist. They serve complementary roles: + +| | GitHub | Jira | +|---|---|---| +| **Primary use** | Code-level actions (PR rebase, code review, implementation) | Issue tracking and project planning | +| **Trigger location** | PR/issue comments on GitHub | Issue comments on Jira | +| **Mission marker** | 📬 | 🎫 | +| **Auth method** | `gh` CLI + `GH_TOKEN` | HTTP Basic + API token | +| **Permission model** | Allowlist + GitHub write access check | Allowlist (email-based) | +| **Polling** | GitHub notifications API | JQL search + comment fetch | + +### Combined configuration + +```yaml +# GitHub integration +github: + nickname: "koan-bot" + commands_enabled: true + authorized_users: ["*"] + +# Jira integration +jira: + enabled: true + base_url: "https://myorg.atlassian.net" + email: "bot@example.com" + nickname: "koan-bot" + authorized_users: ["*"] + projects: + PROJ: myproject # Simple format + INFRA: # Extended format with target branch + project: infrastructure + branch: "11.126" +``` + +```bash +# In .env +GH_TOKEN=ghp_xxxx +KOAN_JIRA_API_TOKEN=xxxx +``` + +Both integrations poll independently during the agent's sleep cycle — GitHub notifications are checked first, then Jira. Each has its own backoff schedule. Missions from both sources enter the same `missions.md` queue and are processed identically by the agent loop. + +### When to use which + +- **GitHub @mentions**: Best for code-centric actions — rebasing a PR, reviewing a diff, implementing a specific issue with linked code context. +- **Jira @mentions**: Best for project-level planning — turning a Jira epic into implementation tasks, planning a feature described in a ticket, auditing code related to a Jira story. + +Both can trigger the same set of commands. The difference is the context URL attached to the mission — a GitHub URL gives the agent direct access to diffs and PR metadata, while a Jira URL provides issue descriptions and comment threads. + +## Troubleshooting + +### Commands not being picked up + +1. **Check feature is enabled**: `jira.enabled: true` in config.yaml +2. **Verify required fields**: `base_url`, `email`, `api_token`, and `nickname` must all be set. Check logs for startup validation warnings. +3. **Check project mapping**: The Jira issue's project key must be in `jira.projects`. A comment on `FOO-123` requires `projects: { FOO: some_project }`. +4. **Check polling**: Look for `[jira]` log entries in `make logs`. If you see "no recently-updated issues found", the JQL query isn't matching. +5. **Verify API access**: Test manually: + ```bash + curl -X POST -u "email@example.com:YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + "https://myorg.atlassian.net/rest/api/3/search/jql" \ + -d '{"jql": "project = FOO", "maxResults": 1}' + ``` + > **Note**: Jira Cloud deprecated `GET /rest/api/3/search` (returns HTTP 410). Koan uses `POST /rest/api/3/search/jql` with cursor-based pagination. + +### Mission queued but not executed + +The 🎫 mission was written to `missions.md`. Check: +- `instance/missions.md` — the mission should be in the Pending section +- Agent loop logs — the mission will be picked up in the next iteration +- Project name resolution — the `repo:` override or project mapping must point to a valid Koan project in `projects.yaml` + +### "No valid project keys after sanitization" + +Jira project keys must be uppercase alphanumeric (e.g., `FOO`, `MYPROJ`). Keys with special characters are silently filtered out. Check your `jira.projects` mapping uses valid keys. + +### Duplicate missions after restart + +Expected behavior. The in-memory processed set is lost on restart, but the persistent tracker (`.jira-processed.json`) prevents most duplicates. If a crash occurred between mission creation and tracker update, a duplicate may appear — it's harmless and the agent handles already-completed missions gracefully. + +## Related + +- [GitHub Notification Commands](github-commands.md) — GitHub @mention integration (complementary) +- [Messaging: Telegram](messaging-telegram.md) — Primary command interface +- [Messaging: Slack](messaging-slack.md) — Alternative messaging provider +- [Messaging: Matrix](messaging-matrix.md) — Alternative messaging provider +- [Skills Reference](skills.md) — Full skill documentation +- [User Manual](user-manual.md) — Complete usage guide diff --git a/docs/maint.md b/docs/maint.md new file mode 100644 index 000000000..a87771ba0 --- /dev/null +++ b/docs/maint.md @@ -0,0 +1,66 @@ +# Maintenance & Release + +## Philosophy + +Kōan has two channels: + +- **`main`** — bleeding edge. Every merged PR lands here. Contributors and adventurous users track this branch. +- **`stable`** — contains *only* tagged releases. Fast-forwarded at each `make release`. Users who want a predictable experience track this. + +A release is cut **when `main` is healthy and something worth shipping has landed** — not on a fixed cadence. Typical triggers: + +- A noteworthy feature is merged and validated. +- A cluster of fixes / polish commits has accumulated (roughly 5–20 commits since the last tag). +- A bug fix on `main` is important enough that stable users need it now. + +Do **not** release if: + +- The test suite is not 100% green. +- Work-in-progress is merged behind feature flags that aren't ready. +- You haven't actually run the code in your own instance since the last tag. + +The human decides. `make release` just enforces the hygiene. + +## Procedure + +```bash +make release +``` + +What it does, in order: + +1. **Preflight** — must be on `main`, clean tree, synced with `origin/main`, `gh` authenticated. +2. **`make test-strict`** — full pytest run. Any failure aborts the release. +3. **Version prompt** — suggests the next patch bump (e.g. `v0.61` → `v0.62`). You can type any valid `vX.Y` or `vX.Y.Z`. +4. **Changelog** — invokes Claude (Haiku) on `git log ..HEAD` to produce a categorized markdown changelog. Falls back to the raw commit list if Claude is unavailable. You can edit it before proceeding. +5. **Confirmation** — nothing is pushed until you confirm. +6. **Tag + push** — `git tag -a vX.Y.Z` with the changelog as the message, then `git push origin vX.Y.Z`. +7. **Fast-forward `stable`** — points `stable` at the new tag and pushes. Creates the branch if it doesn't exist yet. +8. **GitHub release** — `gh release create ... --latest` with the changelog. + +## Version scheme + +Currently `v0.NN` (single minor). When we hit 1.0, switch to semver `vX.Y.Z`: + +- **patch** (`Z`) — fixes, docs, internal refactors +- **minor** (`Y`) — new features, backward-compatible +- **major** (`X`) — breaking changes (config format, skill API, etc.) + +## Hotfix on stable + +If stable needs a fix and `main` has unreleasable work in flight: + +```bash +git checkout -b hotfix/xyz stable +# fix + commit +git checkout main && git cherry-pick hotfix/xyz +# merge PR to main, then: +make release # on main, will fast-forward stable +``` + +Do not commit directly to `stable`. It must only ever be a fast-forward of a tagged commit on `main`. + +## Recovery + +- **Bad tag pushed** — `git tag -d vX.Y && git push origin :refs/tags/vX.Y && gh release delete vX.Y`. Then re-run `make release`. +- **`stable` diverged** — reset it to the latest tag: `git branch -f stable vX.Y && git push --force-with-lease origin stable`. Force-push is acceptable on `stable` *only* to realign it with a tag. diff --git a/docs/messaging-matrix.md b/docs/messaging-matrix.md new file mode 100644 index 000000000..3a5175219 --- /dev/null +++ b/docs/messaging-matrix.md @@ -0,0 +1,123 @@ +# Matrix Setup Guide + +This guide covers setting up Kōan with [Matrix](https://matrix.org) as the messaging provider. Kōan talks to a Matrix homeserver via the Client-Server HTTP API — no extra Python packages are required beyond `requests`. + +## Prerequisites + +- Access to a Matrix homeserver. You can use [matrix.org](https://matrix.org), a self-hosted Synapse/Dendrite/Conduit, or any compliant server. +- A dedicated Matrix account for the bot (recommended — don't reuse your personal account). +- An Element (or other Matrix client) login for the bot account, to invite it into the operating room. + +## Step 1: Create a Bot Account + +Either register a new account directly on the homeserver or use an existing dedicated account. The user ID will look like `@koan:matrix.org`. + +## Step 2: Obtain an Access Token + +The easiest way is to log in via Element with the bot account, then: + +1. Open Element → **Settings → Help & About** +2. Scroll to the bottom and expand **Access Token** +3. Copy the token (long string starting with `syt_`, `mat_`, or similar) + +Alternatively, use the `/login` API endpoint: + +```bash +curl -XPOST -d '{ + "type": "m.login.password", + "user": "koan", + "password": "YOUR_BOT_PASSWORD" +}' "https://matrix.org/_matrix/client/v3/login" +``` + +The response contains an `access_token` field. + +> **Security note:** The access token grants full account access. Treat it like a password — never commit it. If leaked, log out the session via Element (**Settings → Sessions**) to invalidate it. + +## Step 3: Create or Choose a Room + +Pick the room Kōan will operate in. Either: + +- Create a new private room in Element and invite the bot. +- Use an existing room and invite the bot. + +Get the room ID: + +1. In Element, open the room +2. Click the room name → **Settings → Advanced** +3. Copy the **Internal room ID** (e.g., `!abcdefghijk:matrix.org`) + +Make sure the bot account has joined the room (accept the invite from the bot's session, or call `/_matrix/client/v3/join/{roomId}`). + +## Step 4: Configure Kōan + +The recommended approach is to put Matrix settings in `instance/config.yaml`: + +```yaml +messaging: + provider: "matrix" + matrix: + homeserver: "https://matrix.org" + user_id: "@koan:matrix.org" + room_id: "!abcdefghijk:matrix.org" + access_token: "syt_your_token_here" +``` + +> Treat `instance/config.yaml` like a secret file — it's gitignored by default. If you commit your `instance/` directory to a separate private repo, that's fine; never commit the access token to a public repo. + +### Legacy: environment variables + +The four `KOAN_MATRIX_*` env vars are still supported and override `config.yaml` when set. Use them only if you have a workflow built around `.env`: + +```bash +# .env (legacy alternative) +KOAN_MESSAGING_PROVIDER=matrix +KOAN_MATRIX_HOMESERVER=https://matrix.org +KOAN_MATRIX_ACCESS_TOKEN=syt_your_token_here +KOAN_MATRIX_USER_ID=@koan:matrix.org +KOAN_MATRIX_ROOM_ID=!abcdefghijk:matrix.org +``` + +Precedence: env var > `config.yaml` value > error. + +## Step 5: Start Kōan + +```bash +make start +``` + +You should see in the logs: + +``` +[init] Messaging provider: MATRIX, Channel: !abcdefghijk:matrix.org +``` + +## How it works + +- **Sending**: `PUT /_matrix/client/v3/rooms/{roomId}/send/m.room.message/{txnId}` with `msgtype: m.text`. Long messages are chunked to 4000 characters per event. +- **Receiving**: Long-polls `GET /_matrix/client/v3/sync` with a 30-second timeout. The first sync discards historical events and records the `next_batch` cursor; subsequent syncs return only new events. +- **Filtering**: Only `m.room.message` events with `msgtype: m.text` are surfaced. Messages sent by the bot's own user ID are ignored so it doesn't reply to itself. + +## Troubleshooting + +### "Missing required settings" + +All four values (`homeserver`, `access_token`, `user_id`, `room_id`) must be set — either under `messaging.matrix` in `instance/config.yaml` or via the corresponding `KOAN_MATRIX_*` env vars. + +### `[matrix] API error 401` / `403` + +- The access token is invalid or has been revoked. Generate a new one (Step 2). +- The bot account isn't joined to the room. Accept the invite first. + +### `[matrix] API error 404` + +- The room ID is wrong, or the homeserver doesn't know about it. +- Ensure the room ID starts with `!` and includes the homeserver suffix (e.g., `!abc:matrix.org`). + +### Bot replies to its own messages + +- Double-check `KOAN_MATRIX_USER_ID` exactly matches the bot's user ID (including the leading `@` and the homeserver part). + +### Encrypted rooms + +This integration uses unencrypted Matrix rooms. End-to-end encryption (Olm/Megolm) is not implemented — using an E2EE room means messages will appear as undecryptable events. Either disable encryption on the room or create a fresh unencrypted room for the bot. diff --git a/docs/provider-claude.md b/docs/provider-claude.md index e9ab0933f..5f0468c5d 100644 --- a/docs/provider-claude.md +++ b/docs/provider-claude.md @@ -122,13 +122,87 @@ projects: ### MCP (Model Context Protocol) Servers Claude Code supports MCP servers for extended capabilities (browser, -databases, APIs): +databases, APIs). Add MCP config file paths to `config.yaml`: ```yaml +# config.yaml — global MCP servers for all projects mcp: - "/path/to/mcp-config.json" ``` +Per-project overrides are supported in `projects.yaml` — a project-level +`mcp` list replaces the global list entirely: + +```yaml +# projects.yaml — project-specific MCP servers +projects: + my-project: + path: "/home/user/my-project" + mcp: + - "/path/to/project-specific-mcp.json" +``` + +The MCP config files use the standard Claude Code JSON format (same as +`~/.claude/mcp.json` or `--mcp-config` flag). + +#### Permissions for MCP Tools + +When Koan runs as a systemd service (or any non-interactive context), +Claude CLI cannot prompt for tool approval. MCP tools will be +**silently denied** unless pre-approved. + +> **Note:** `skip_permissions: true` does **not** work when Koan runs +> as root — Claude CLI rejects `--dangerously-skip-permissions` with +> root/sudo privileges. You must use the allowlist approach below. + +To pre-approve MCP tools, create a `.claude/settings.local.json` file +**in the target project's root directory** (the `path` from +`projects.yaml`). This file is loaded by Claude CLI when it runs with +that project as its working directory. + +Example — allowlisting the Atlassian MCP server's Jira tools: + +```json +{ + "permissions": { + "allow": [ + "mcp__atlassian__getAccessibleAtlassianResources", + "mcp__atlassian__getJiraIssue", + "mcp__atlassian__searchJiraIssuesUsingJql", + "mcp__atlassian__getVisibleJiraProjects", + "mcp__atlassian__getJiraIssueTypeMetaWithFields", + "mcp__atlassian__getJiraProjectIssueTypesMetadata", + "mcp__atlassian__createJiraIssue", + "mcp__atlassian__editJiraIssue", + "mcp__atlassian__addCommentToJiraIssue", + "mcp__atlassian__getTransitionsForJiraIssue", + "mcp__atlassian__transitionJiraIssue", + "mcp__atlassian__lookupJiraAccountId", + "mcp__atlassian__getIssueLinkTypes", + "mcp__atlassian__createIssueLink", + "mcp__atlassian__getJiraIssueRemoteIssueLinks", + "mcp__atlassian__searchAtlassian", + "mcp__atlassian__fetchAtlassian", + "mcp__atlassian__atlassianUserInfo" + ] + } +} +``` + +The tool name format is `mcp____` where +`` matches the key in your MCP config JSON (e.g., +`"atlassian"` in `~/.claude.json`). To find the exact tool names, +run Claude CLI interactively once — denied tools appear in the JSON +output under `permission_denials`. + +**Setup checklist for each project using MCP:** + +1. Add the MCP config path to `projects.yaml` (under the project's + `mcp:` key) or globally in `config.yaml` +2. Create `/.claude/settings.local.json` with the + tool allowlist +3. Restart Koan (`systemctl restart koan.service`) + ### Max Turns The `max_turns` setting controls how many tool-use rounds Claude gets diff --git a/docs/rtk.md b/docs/rtk.md new file mode 100644 index 000000000..833750bcc --- /dev/null +++ b/docs/rtk.md @@ -0,0 +1,116 @@ +# RTK integration + +Kōan can optionally lean on [`rtk`](https://github.com/rtk-ai/rtk) — a Rust CLI proxy that compresses common dev-command output (`git`, `ls`, `cat`, `grep`, `pytest`, `cargo`, `gh`, `docker`, …) by 60–90 % before it reaches Claude. Strictly complementary to the [caveman optimisation](../instance.example/config.yaml): caveman trims what Claude **writes**; rtk trims what Claude **reads**. + +`rtk` is **never** a Kōan dependency. If it isn't installed, nothing changes. + +## How it plugs in + +Three layers, each independently useful: + +| Layer | What it does | Activation | +|---|---|---| +| **L1 — Detection** | At boot, log whether `rtk` and `jq` are present and whether the `~/.claude/settings.json` PreToolUse hook is wired up. | Always on (read-only probe). | +| **L2 — Awareness** | Inject `koan/system-prompts/rtk-awareness.md` into Claude's system prompt so Claude prefers `rtk git status` over `git status`. | Default `auto` — on iff the binary is detected. | +| **L3 — Hook setup** | The `/rtk setup` Telegram skill runs `rtk init -g --auto-patch` to install the official PreToolUse hook (transparent rewrite of every Bash command). | Manual — never automatic. | + +## Quick start + +```bash +# 1. Install rtk on the host (one-time) +brew install rtk +# or: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh + +# 2. Restart Kōan — boot log should show: +# [init] rtk 0.28.2 detected, hook: inactive + +# 3. (optional) From Telegram, install the auto-rewrite hook: +/rtk setup # preview +/rtk setup confirm # actually run rtk init -g --auto-patch +``` + +After step 3, every Bash command Claude runs inside a Kōan mission gets transparently rewritten to its `rtk` equivalent. Nothing changes in Kōan's argv or prompt assembly — the hook fires inside Claude Code itself. + +## The `/rtk` skill + +| Command | Effect | +|---|---| +| `/rtk` | Show detection status (binary, version, hook, jq, project gate) | +| `/rtk setup` | Preview what `rtk init -g --auto-patch` would change | +| `/rtk setup confirm` | Actually install the PreToolUse hook | +| `/rtk uninstall` | Run `rtk init -g --uninstall` | +| `/rtk gain [args]` | Forward to `rtk gain` (analytics — token savings, history, daily) | +| `/rtk discover [args]` | Forward to `rtk discover` (find missed savings opportunities) | +| `/rtk on` / `/rtk off` | Runtime override — toggles awareness without editing `config.yaml`. Writes `instance/.koan-rtk-override`. | + +## Configuration + +```yaml +# instance/config.yaml +optimizations: + rtk: + enabled: auto # auto | true | false + # auto = on iff `rtk` is on PATH (default) + awareness: true # inject the awareness section into system prompts + require_jq: true # warn at boot if jq is missing +``` + +```yaml +# projects.yaml — per-project opt-out +projects: + myproject: + rtk: false # never inject awareness for this project +``` + +Resolution order for `is_rtk_mode()`: + +1. `instance/.koan-rtk-override` (`/rtk on` / `/rtk off`) — highest priority. +2. `optimizations.rtk.enabled` in `config.yaml`. +3. `auto` → fall through to `app.rtk_detector.detect_rtk()`. + +Per-project resolution (`get_project_rtk_enabled`): +- `projects..rtk: true` or `false` → hard override for that project. +- Anything else (or omitted) → defer to global `is_rtk_mode()`. + +## What rtk filters and what it doesn't + +The hook only intercepts the **Bash tool** — Claude Code's native `Read` / `Glob` / `Grep` bypass it. The awareness section nudges Claude to prefer `rtk read ` and `rtk grep ` for large files, but agents may still default to native tools, capping practical savings below the headline 80 %. + +Filters exist for: + +- Git: `git status`, `git log`, `git diff`, `git add`, `git commit`, `git push`, `git pull` +- Files: `ls`, `cat`/`read`, `find`, `grep`, `diff` +- GitHub: `gh pr list/view`, `gh issue list`, `gh run list` +- Tests: `pytest`, `jest`, `vitest`, `cargo test`, `go test`, `rspec`, `playwright test`, generic `rtk test ` +- Build/lint: `tsc`, `ruff check`, `cargo build/clippy`, `golangci-lint`, `eslint/biome`, `rubocop` +- Containers: `docker ps`, `docker logs`, `kubectl pods/logs` +- Cloud: `aws sts/ec2/lambda/logs/cloudformation/dynamodb/iam/s3` +- Misc: `log`, `json`, `curl`, `env` + +Unknown commands pass through unchanged — rtk is never destructive. + +## Caveats + +- **Never auto-patches `~/.claude/settings.json`.** Hook installation only happens via explicit `/rtk setup confirm`. +- **`jq` is required for the hook script.** The detector probes for it independently of `rtk`. If missing, `/rtk` warns but the awareness section still works (Claude calls `rtk` directly via Bash). +- **Telemetry is opt-in.** rtk has its own anonymous usage telemetry, off by default. Kōan never enables it on the user's behalf. +- **Copilot provider is out of scope (v1).** rtk's Copilot support is `deny-with-suggestion` rather than transparent rewrite — friction outweighs savings. Skip the Copilot path for now. +- **Windows native is degraded.** rtk's hook is Unix-only; the awareness section still works. + +## Verifying + +```bash +# Without rtk on PATH: +python -c "from app.rtk_detector import detect_rtk; print(detect_rtk())" +# RtkStatus(installed=False, ...) + +# With rtk installed: +rtk --version # rtk 0.28.2 +KOAN_ROOT=/path .venv/bin/pytest koan/tests/test_rtk_detector.py koan/tests/test_rtk_skill.py -v +``` + +## Related + +- Issue [#1295](https://github.com/Anantys-oss/koan/issues/1295) — the integration plan. +- Issue [#1279](https://github.com/Anantys-oss/koan/issues/1279) — caveman mode (composes orthogonally). +- Modules: `koan/app/rtk_detector.py`, `koan/app/prompt_builder.py` (`_get_rtk_section`), `koan/skills/core/rtk/`. diff --git a/docs/skills.md b/docs/skills.md index 303a65099..376b8b8e0 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -86,6 +86,7 @@ Skills marked **GitHub @mention** can be triggered by commenting `@koan-bot ` | `/lng`, `/fr`, `/en` | Set reply language preference | | `/verbose` | — | Enable real-time progress updates | | `/silent` | — | Disable real-time progress updates | +| `/config_check` | `/cfgcheck`, `/configcheck` | Detect drift between instance/config.yaml and the template | ## System @@ -109,8 +110,15 @@ Install skills from Git repos: ``` /skill install https://github.com/your-org/koan-skills.git +/skill approve /skill update /skill remove ``` +New installs and `/scaffold_skill` output are **quarantined** behind an +approval gate — the registry will not load them until `/skill approve` is run +with the fingerprint shown in the install reply. Inspect the cloned files +before approving. Set `skills.allowed_hosts` in `config.yaml` to restrict +which Git hosts `/skill install` can fetch from. + Or create your own in `instance/skills///` with a `SKILL.md` file. See [koan/skills/README.md](../koan/skills/README.md) for the full authoring guide. diff --git a/docs/user-manual.md b/docs/user-manual.md index be06f1bfc..ead7bd015 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -93,7 +93,7 @@ Pending → In Progress → Done ✓ ``` 1. **Pending** — Queued and waiting. Kōan picks missions from the top of the queue. -2. **In Progress** — Kōan is actively working on it via Claude Code CLI. +2. **In Progress** — Kōan is actively working on it via the configured CLI provider. 3. **Done** — Completed successfully. Code is in a `koan/*` branch, often with a draft PR. 4. **Failed** — Something went wrong. Kōan logs the reason and moves on. @@ -141,6 +141,11 @@ If Kōan misclassifies your message, use `/chat` to force chat mode: - `/cancel auth` — Cancel the mission matching "auth" +**`/abort`** — Abort the current in-progress mission and move to the next one. + +- **Usage:** `/abort` +- The running Claude subprocess is killed, the mission is moved to Failed, and the agent loop picks the next pending item. + **`/priority`** — Move a pending mission to a different position in the queue. - **Usage:** `/priority ` (move to top) or `/priority ` @@ -177,22 +182,40 @@ If Kōan misclassifies your message, use `/chat` to force chat mode: - `/live` — Check what Kōan is doing right now during a long mission -**`/logs`** — Show the last 10 lines from run.log and awake.log, formatted in code blocks. +**`/logs [run|awake|all]`** — Show the last 20 lines from log files, formatted in code blocks. + +- **Default:** Shows only `run.log`. Use `awake` for bridge logs, `all` for both.
Use cases -- `/logs` — Quick check of recent agent and bridge output without SSH access +- `/logs` — Quick check of recent agent output (run.log only) +- `/logs awake` — Check bridge/Telegram polling output +- `/logs all` — See both run and awake logs
-**`/quota`** — Check remaining API quota (live, no cache). +**`/quota [remaining_%]`** — Check remaining API quota (live, no cache), or override the internal estimate. - **Aliases:** `/q`
Use cases -- `/quota` — See how much API budget is left before adding heavy missions +- `/quota` — See how much API budget is left before adding heavy missions, plus the rolling burn rate (%/h) and estimated time to exhaustion +- `/quota 32` — Tell Kōan it has 32% remaining (fixes drift when internal estimate is wrong) +- If Kōan is paused due to quota but the API is actually available, `/quota 50` will correct the estimate and clear the pause +- When the burn rate predicts session exhaustion in less than 30 min, the autonomous mode is automatically downgraded one tier (deep→implement→review). A Telegram alert fires once when projected exhaustion is under 60 min and the next quota reset is still more than 2 h away. +
+ +**`/check_notifications`** — Force an immediate check of GitHub and Jira notifications, bypassing the exponential backoff timer. + +- **Aliases:** `/read` + +
+Use cases + +- `/read` — When the queue is empty and you know there are pending notifications +- `/check_notifications` — After posting a GitHub comment that should trigger a mission
**`/verbose`** / **`/silent`** — Toggle real-time progress updates. When verbose is on, Kōan sends progress messages as it works. @@ -253,6 +276,63 @@ Kōan can manage multiple projects simultaneously. It rotates between them based - `/unfocus` — "OK, back to normal" +**`/passive`** — Enter passive (read-only) mode. The agent loop keeps running (heartbeat, GitHub notification polling, Telegram commands) but never executes missions or autonomous work. Missions accumulate as Pending. + +- **Usage:** `/passive [duration]` — no duration = indefinite +- **Examples:** `/passive`, `/passive 4h`, `/passive 2h30m` + +**`/active`** — Exit passive mode and resume normal execution. Queued missions drain naturally. + +
+Use cases + +- `/passive` — "I'm at the desk, don't touch anything" +- `/passive 4h` — "Hands off for the next 4 hours" +- `/active` — "I'm done, you can work again" +
+ +### Permanent Focus Mode + +Focus mode can be made permanent via config, turning Kōan into a pure mission executor. When enabled, the agent only runs missions you explicitly queue — it never picks up GitHub issues autonomously, never runs contemplative reflection, and never enters DEEP mode. The loop keeps polling Telegram, GitHub notifications, and recurring schedules, so it still wakes up the moment you queue something. + +This extends the `/focus` Telegram command (which is time-bounded) into a permanent config-level switch. + +- **Enable globally in `instance/config.yaml`:** + ```yaml + focus: true + ``` +- **Or via environment variable:** `KOAN_FOCUS=1` (takes precedence over `config.yaml`). +- **Per-project in `projects.yaml`:** + ```yaml + defaults: + focus: true # All projects focused by default + projects: + myapp: + focus: false # Override: allow autonomous work on myapp + ``` +- **Disable:** set back to `false`, or `KOAN_FOCUS=0`. + +What continues to run under focus mode: + +- Missions queued via `/mission`, GitHub `@mention` commands, and recurring schedules. +- Heartbeat, auto-update, Telegram polling, GitHub notification polling, CI queue drain. + +What is disabled: + +- DEEP mode (capped at `implement`). +- Contemplative sessions (random reflection rolls are skipped). +- Autonomous exploration (the loop idles with wake-on-mission when no mission is pending). +- The agent prompt's `GitHub Issue Selection` section is replaced with an explicit "do not pick up issues" instruction. + +**How it differs from `/passive`:** passive mode blocks all execution (missions sit as Pending until you `/active`). Focus mode keeps the executor running for any mission you queue — it only gates *autonomous work selection*. + +**When to use:** + +- You want Kōan to act strictly on demand, no surprises on the PR list. +- You're handing off mission dispatch to another system (CI, a team workflow) and want Kōan to be a quiet executor. +- Multi-bot setups where only one instance should pick up issues autonomously. +- Per-project: focus some repos while allowing exploration on others. + --- ## Intermediate — Productivity Workflows @@ -261,7 +341,42 @@ These features turn Kōan from a task runner into a full development workflow pa ### Code Operations -**`/brainstorm`** — Decompose a broad topic into multiple linked GitHub issues grouped under a master tracking issue. +**`/brainstorm`** — Decompose a broad topic into 3-8 high-leverage GitHub sub-issues grouped under a master tracking issue. + +The decomposer runs as a senior-engineer-style ideation pass: it explores the codebase (if provided) or external source, hunts for compounding improvements, and refuses to pad with generic refactors. Every sub-issue body follows this template: + +```markdown +## Why This Matters + + +## Approach + + +## Acceptance Criteria +- [ ] Criterion 1 +- [ ] Criterion 2 + +## Risks & Caveats + + +## Scores +- Impact: ████████░░ 8/10 +- Difficulty: ██████░░░░ 6/10 +- Short-Term ROI: ███████░░░ 7/10 +- Long-Term Value: █████████░ 9/10 + +## Priority +Immediate | Prototype First | Research Further | Skip + +## Dependencies + +``` + +The master tracking issue then synthesizes the set with three optional sections: + +- **Top Ranked** — sub-issues ordered by ROI / feasibility / strategic value, each with a one-line rationale. +- **Fast Wins** — bucketed by horizon: `< 1 day`, `< 1 week`, `< 1 month`. +- **Overall Assessment** — short critical verdict on whether the initiative is worth pursuing and what to prioritize. - **Usage:** `/brainstorm `, `/brainstorm `, `/brainstorm --tag