diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..6aa416628 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,86 @@ +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 + name: release + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - 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 tag --sort=-v:refname --list 'v*' | head -1) + 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..2292b9705 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: πŸ§ͺ Tests on: push: @@ -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,25 +37,25 @@ 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 - - name: Install dependencies + - 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 }}) + - name: ▢️ Run tests (${{ matrix.group.name }}) if: ${{ !inputs.group || matrix.group.name == inputs.group }} working-directory: koan env: @@ -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..a846aed92 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ .venv/ venv/ .coverage +.coverage.* +htmlcov/ +koan/htmlcov/ # Runtime .koan-status @@ -52,3 +55,4 @@ projects.docker.yaml docker-compose.override.yml .env.docker claude-auth/ +.spec/ 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..fd202a4a3 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,10 @@ 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) +- **`config.py`** β€” Centralized configuration loading and access: tool config, model selection, Claude CLI flag building, behavioral settings, auto-merge config +- **`constants.py`** β€” Centralized numeric constants for the agent loop (thresholds, timeouts, tuning parameters). Import-as pattern preserves module-level attribute names for test compatibility. +- **`run_log.py`** β€” Shared colored logging wrapper (`log_safe(category, msg)`). Replaces per-module `_log_*` helpers. +- **`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. @@ -59,9 +67,12 @@ Communication between processes happens through shared files in `instance/` with - **`loop_manager.py`** β€” Focus area resolution, pending.md creation, interruptible sleep with wake-on-mission, project validation - **`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. +- **`prompt_builder.py`** β€” Agent prompt assembly for the agent loop. Includes budget-aware context trimming. +- **`event_scheduler.py`** β€” One-shot datetime-scheduled mission triggers. Reads `instance/events/*.json`, fires missions on schedule. +- **`suggestion_engine.py`** β€” Automation suggestion engine: surfaces recurring/schedule system recommendations with copy-pasteable commands +- **`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 +87,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 @@ -94,28 +106,34 @@ Communication between processes happens through shared files in `instance/` with - **`github_command_handler.py`** β€” Bridges GitHub @mention notifications to missions: validate command β†’ check permissions β†’ react β†’ create mission - **`rebase_pr.py`** β€” PR rebase workflow - **`recreate_pr.py`** β€” PR recreation: fetch metadata/diff, create fresh branch, reimplement from scratch -- **`claude_step.py`** β€” Shared helpers for git operations and Claude CLI invocation (used by pr_review, rebase_pr, recreate_pr) +- **`claude_step.py`** β€” Shared helpers for git operations and Claude CLI invocation (used by pr_review, rebase_pr, recreate_pr). Also provides `run_ci_fix_loop()` β€” shared CI fix loop with configurable recheck semantics (polling vs single-shot) via `use_polling` flag and caller-specific `prompt_builder` callable. +- **`remote_rename_detector.py`** β€” Detects and fixes renamed GitHub remotes in workspace projects +- **`head_tracker.py`** β€” Detects remote HEAD branch changes (e.g. master β†’ main) and updates local workspace. State persisted in `instance/.head-tracker.json`, throttled to once per 12h. Integrated into startup, manual trigger via `/rescan`. **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 +- **`prompts.py`** β€” System prompt loader; `load_prompt()` for `koan/system-prompts/*.md`, `load_skill_prompt()` for skill-bound prompts. Supports `{@include partial-name}` directive for reusable prompt fragments from `koan/system-prompts/_partials/`. - **`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`) +- **`security_review.py`** β€” Differential security review on mission diffs: blast radius analysis, risk classification, journal logging. Runs before auto-merge decisions. +- **`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/` (abort, ai, audit, cancel, chat, check, check_need, check_notifications, claudemd, config_check, delete_project, doc, fix, focus, idea, implement, inbox, journal, language, list, live, magic, mission, passive, plan, pr, priority, private_security_audit, projects, quota, rebase, recreate, recurring, refactor, reflect, rescan, review, review_rebase, rtk, security_audit, shutdown, sparring, spec_audit, 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. - **`github_enabled: true`** flag marks skills that can be triggered via GitHub @mentions. **`github_context_aware: true`** means the skill accepts additional context after the command. +- **Combo skills**: `sub_commands` field in SKILL.md frontmatter defines skills that decompose into multiple sub-missions (e.g., `/review_rebase` queues both `/review` and `/rebase`). `collect_combo_skills()` in `skills.py` discovers these dynamically from the registry. - **Prompt-only skills**: omit `handler`, put prompt text after the frontmatter β€” sent to Claude directly. - See `koan/skills/README.md` for the full authoring guide. @@ -128,8 +146,23 @@ Extensible command plugin system. Each skill lives in `skills///*` branches** (default `koan/`, configurable via `branch_prefix` in `config.yaml`), never commits to main @@ -138,8 +171,28 @@ 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()`. +- **No inline prompts in Python code** β€” LLM prompts MUST be extracted to `.md` files. Skill-bound prompts go 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()`. Reusable prompt fragments live in `koan/system-prompts/_partials/` and are included via `{@include partial-name}` directive (resolved at load time by `prompts.py`). - **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/Dockerfile b/Dockerfile index 6ba0331e3..dcaebccac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ bash \ procps \ openssh-client \ + bubblewrap \ make \ nodejs \ npm \ 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..a9cecaea1 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.** @@ -47,6 +53,7 @@ This isn't a chatbot wrapper. It's a collaborator with memory, personality, and ```bash git clone https://github.com/sukria/koan.git cd koan +make setup make install # Interactive web wizard β€” sets up everything make start # Launches the full stack make logs # Watch it work @@ -92,7 +99,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 +109,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 +133,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. @@ -154,13 +161,16 @@ 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) +- **Security review** β€” Automatic diff analysis for dangerous patterns (eval, shell injection, hardcoded secrets, etc.) before auto-merge. Configurable risk threshold and blocking behavior per project - **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 @@ -270,6 +280,9 @@ Define your projects in `projects.yaml` at `KOAN_ROOT`: defaults: git_auto_merge: enabled: false + security_review: + enabled: true # Scan diffs for dangerous patterns before merge + blocking: false # Set to true to block auto-merge on high risk projects: webapp: @@ -281,6 +294,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 +312,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 +336,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 +365,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 +400,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/docker-entrypoint.sh b/docker-entrypoint.sh index aebeebb73..5f4ca637a 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -63,6 +63,13 @@ verify_binaries() { success "claude $(claude --version 2>/dev/null | head -1 || echo '(unknown version)')" fi ;; + codex) + if ! command -v codex &>/dev/null; then + missing+=("codex (OpenAI Codex CLI) β€” npm install -g @openai/codex may have failed") + else + success "codex $(codex --version 2>/dev/null | head -1 || echo '(unknown version)')" + fi + ;; copilot) if ! command -v github-copilot-cli &>/dev/null && ! command -v copilot &>/dev/null; then missing+=("github-copilot-cli or copilot (GitHub Copilot CLI)") @@ -70,7 +77,7 @@ verify_binaries() { success "copilot CLI" fi ;; - local|ollama) + local|ollama|ollama-launch) if ! command -v ollama &>/dev/null; then missing+=("ollama") else @@ -125,6 +132,41 @@ check_claude_auth() { return 1 } +# Check Codex authentication via a minimal probe. +# Returns 0 if authenticated, 1 if not. +check_codex_auth() { + # Option 1: OpenAI API key + if [ -n "${OPENAI_API_KEY:-}" ]; then + success "Codex auth: OPENAI_API_KEY" + return 0 + fi + + # Option 2: Device auth / interactive login β€” probe with a tiny prompt + # --skip-git-repo-check: /app may not be a trusted git dir in Docker + # --sandbox workspace-write: replaces deprecated --full-auto + if timeout 15 codex exec --skip-git-repo-check --sandbox workspace-write "ok" >/dev/null 2>&1; then + success "Codex auth: interactive login" + return 0 + fi + + error "Codex CLI is not authenticated" + log " Option 1: Set OPENAI_API_KEY in .env" + log " Option 2: Run 'codex login --device-auth' inside the container" + return 1 +} + +# Dispatch to the correct auth check based on configured provider. +check_provider_auth() { + local provider="${KOAN_CLI_PROVIDER:-claude}" + case "$provider" in + claude) check_claude_auth ;; + codex) check_codex_auth ;; + # Other providers (copilot, local, ollama) don't have a standard + # auth check β€” verify_auth() handles their warnings. + *) return 0 ;; + esac +} + verify_auth() { local provider="${KOAN_CLI_PROVIDER:-claude}" local warnings=() @@ -227,7 +269,7 @@ case "$COMMAND" in start) printf "${BOLD}${CYAN}Kōan Docker β€” initializing${RESET}\n" verify_binaries || exit 1 - check_claude_auth || exit 1 + check_provider_auth || exit 1 verify_auth setup_ssh setup_instance @@ -243,7 +285,7 @@ case "$COMMAND" in agent) log "Kōan Docker β€” agent only" verify_binaries || exit 1 - check_claude_auth || exit 1 + check_provider_auth || exit 1 verify_auth setup_ssh setup_instance diff --git a/docs/github-commands.md b/docs/github-commands.md index c04d7aec8..46f094f66 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,35 @@ 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. + +#### Multiple instances + +When several Kōan instances share the same GitHub account (each watching a different set of repos), @mentions on repos not in this instance's `projects.yaml` trigger warnings by default. Suppress them with: + +```yaml +enable_multiple_instances: true +``` + +This is a top-level config key (not nested under `github:`). When enabled, Kōan silently skips @mentions from unregistered repos instead of logging warnings and sending Telegram alerts β€” the assumption is that another instance handles them. Notifications from unregistered repos are left unread so sibling instances can process them. + +When `enable_multiple_instances` is **false** (default, single-instance mode), notifications from unregistered repos are automatically marked as read to prevent inbox accumulation β€” no other instance will claim them. + ### 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 +144,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 +243,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 +253,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 +320,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/memory-injection.md b/docs/memory-injection.md new file mode 100644 index 000000000..8876afdd6 --- /dev/null +++ b/docs/memory-injection.md @@ -0,0 +1,390 @@ +# Memory Injection into Skill Prompts + +Status: shipped on commit `d9dfab2` β€” `feat(memory): wire memory into skill prompts`. + +This document describes how Kōan's per-project memory is assembled and threaded +through both the autonomous agent loop and the five mission-driving skills +(`/fix`, `/plan`, `/implement`, `/refactor`, `/review`), plus the two growth- +control tricks that ship with it. + +--- + +## 1. Problem this solves + +Before this commit, project memory had two structural holes: + +| Hole | Symptom | +|------|---------| +| **Skills bypassed memory** | The five skills built their own prompts and saw none of `learnings.md`. Only the autonomous agent loop benefited from it. `/fix issue-42` on a project that had spent months teaching the agent to never touch the migrations folder got no warning. | +| **Two template files were inert** | `memory/projects/_template/context.md` and `priorities.md` shipped in the template but were never read or written by any code path. Operators who filled them in saw no behavioral change. | + +A third issue lived in the growth-control side: the periodic 24h semantic +compaction used **exact-string** dedup for incoming PR-review lessons, so any +paraphrase ("test PR changes" vs. "verify changes with tests") accumulated +between compaction cycles. And compaction itself ran unconditionally on every +schedule tick β€” even when the file had barely grown, burning lightweight-model +quota for negligible savings. + +--- + +## 2. What ships + +### 2.1 A single shared memory-injection helper + +New module **`koan/app/skill_memory.py`** β€” sole source of truth for "give me a +memory block for this project, scoped to this task." It produces an XML-fenced +block: + +``` + +# Project Memory + +## Context (human-curated) + + +## Priorities (human-curated) + + +## Learnings (filtered β€” K of N) + + +``` + +Two public entry points: + +- `build_memory_block(instance, project_name, task_text, ...)` β€” the canonical + builder, used by the agent loop via `prompt_builder._get_learnings_section`. +- `build_memory_block_for_skill(project_path, task_text, **kwargs)` β€” + convenience wrapper used by skill runners. Resolves `KOAN_ROOT` from the + environment and **reverse-resolves `project_path` against `projects.yaml`** + so operators whose configured slug differs from the repo directory name + (e.g. `path: ~/code/koan-fork` mapped to `name: koan`) still get memory + injected. + +Source rules: + +| Source | Loading | Filtering | Cap | +|--------|---------|-----------|-----| +| `learnings.md` | Jaccard scoring vs. task text via `memory_recall.score_and_select` | Honors `[recall:full]` escape hatch | `memory.max_relevant_learnings` (default 25 for skills, 40 for agent loop) | +| `context.md` | Verbatim | None | 80 lines | +| `priorities.md` | Verbatim | None | 40 lines | + +If every source is empty/missing, the helper returns `""` so callers can +unconditionally substitute the placeholder. + +A defensive guard (`_is_safe_project_name`) rejects `..`, path separators, and +leading-dot names β€” today every caller is operator-controlled, but the function +is the chokepoint for any future untrusted input. + +### 2.2 Skills now inject memory + +Each of the five mission-driving skills calls the helper and passes the result +as a `{PROJECT_MEMORY}` placeholder into its prompt: + +| Skill | Runner | Prompt(s) | +|-------|--------|-----------| +| `/fix` | `koan/skills/core/fix/fix_runner.py` | `fix.md` | +| `/implement` | `koan/skills/core/implement/implement_runner.py` | `implement.md` | +| `/plan` | `koan/app/plan_runner.py` (new + iterate paths, plus review loop) | `plan.md`, `plan-iterate.md` | +| `/review` | `koan/app/review_runner.py` | `review.md`, `review-architecture.md`, `review-with-plan.md` | +| `/refactor` | _(via the agent loop)_ | inherits the agent.md path | + +For `/review`, scoring uses **title + body + first 2K chars of diff** instead +of "title + branch" β€” branch names like `koan/fix-issue-123` produce near-zero +Jaccard signal, while the diff is where the file paths and module names that +the learnings file actually indexes against live. + +### 2.3 Agent loop now sees `context.md` and `priorities.md` + +`prompt_builder._get_learnings_section()` delegates to the new helper (with +defaults of 40 / 5 instead of 25 / 3 because the agent loop has more prompt +headroom). The visible change to operators: the existing "Project Learnings +(filtered)" section is now a richer `` block with sub-sections +per source, plus the two template files actually do something. + +The agent-loop section in `koan/system-prompts/agent.md` was rewritten to +document all three sources and tell Claude that `context.md`/`priorities.md` +are human-only territory. + +### 2.4 Anti-thrash guard on semantic compaction + +`memory_manager.compact_learnings()` now skips when running the compaction CLI +would barely move the needle. + +Two flavours, in priority order: + +1. **Growth-aware** β€” when prior state knows how many lines the file held + right after the last successful compaction, skip if the file has grown by + less than 10% relative to that baseline. This is the most accurate signal + ("almost nothing has been added since last cycle"). +2. **Target-distance fallback** β€” when there's no prior telemetry (first + compaction ever, legacy plain-hash state, or non-dict JSON), skip if + `(original - max_lines) / original < 10%`. + +The state file format upgraded from a plain hex hash to JSON: + +```json +{"hash": "", "compacted_lines": 87, "updated_at": "2026-05-17T..."} +``` + +Legacy plain-hash files are tolerated and rewritten in JSON on the next +successful compaction. Skipped runs return `{"skipped": true, "reason": +"anti_thrash"}` in the stats dict so `run_cleanup()` can distinguish them +from "no change" skips. + +### 2.5 Write-time semantic dedup for PR-review lessons + +`pr_review_learning._append_lessons_to_learnings()` runs a two-pass dedup: + +1. **Exact-string** against existing lines (cheap, always runs). Drops any + candidate that already appears verbatim. +2. **Semantic** via a lightweight Claude pass (15s timeout, 1 turn, + `max_attempts=1`) on the candidates that survive pass 1. Catches + paraphrases β€” "test PR changes" vs. "verify changes with tests". A final + exact-string sweep absorbs any echoed existing line from the CLI output. + +Gated by `memory.write_time_dedup` in `config.yaml` (default `true`). Falls +back transparently to pass-1-only dedup on CLI failure or timeout. Skipped +entirely when the existing file is empty or `project_path` is unknown. + +The prompt lives in `koan/system-prompts/learnings-dedup.md` and explicitly +tells the model to **preserve exact wording** and to **keep on doubt** β€” the +periodic semantic compaction pass will merge what slips through. + +### 2.6 Themed compaction output + +`koan/system-prompts/learnings-compaction.md` was updated to organize the +compacted output into themed sections: + +``` +## Conventions +## Gotchas +## Rejected-PR lessons +## Architecture notes +## Other (fallback when nothing fits the four themes above) +``` + +Empty sections are not emitted. This is purely a presentation change β€” same +lines come out, grouped differently β€” but it makes the file dramatically +easier for a human to skim, and gives the Jaccard scorer better local +neighborhoods to draw from. + +### 2.7 Config knob + +```yaml +# instance.example/config.yaml +memory: + write_time_dedup: true # default; set to false to save quota +``` + +--- + +## 3. Benefits + +- **Skills now align with project conventions.** A `/fix` on a project where + the agent has previously learned "never edit `instance/`" sees that rule in + its prompt. Before, only the autonomous loop saw it. +- **Two dead template files become first-class memory.** Operators who fill + in `context.md` (architecture, stakeholders) and `priorities.md` (current + focus, no-touch zones) finally get the behavioral payoff. Both files are + marked human-only β€” the compaction pipeline ignores them, so notes you + write stay exactly as written. +- **Single source of truth for memory assembly.** Before, the agent loop had + its own learnings-loading code and the skills had nothing; now both paths + go through `skill_memory.py`. One bug to fix, one set of caps to tune. +- **Quota saved on idle compaction cycles.** The anti-thrash guard skips the + ~120s lightweight-model call when the file has barely grown. On a quiet + day across N projects, that's NΓ—120s/day of quota recovered. +- **Paraphrased duplicates die at the write boundary.** Write-time semantic + dedup prevents lessons.md from drifting into "five ways to say the same + thing" between 24h compaction cycles. +- **Cross-instance portability for project slugs.** The reverse-lookup + against `projects.yaml` means forks/clones whose directory name doesn't + match the configured project name still get memory injected. +- **Better Jaccard signal for `/review`.** Scoring against title + body + + diff slice (not branch name) means the right lessons surface for the right + PRs, not whichever lessons happen to share a word with `koan/fix-issue-N`. +- **No regression in the cache prefix.** The memory block lives in the + *user* prompt (varies per mission) so it doesn't poison the prefix-cached + system prompt β€” that split is preserved by `build_agent_prompt_parts`. + +--- + +## 4. Limits + +- **Jaccard is dumb.** Word-overlap scoring will miss semantically related + lessons that don't share tokens with the task text, and will surface + unrelated lessons that happen to share common words. `[recall:full]` is + the escape hatch, but it costs prompt budget. A true embedding-based + recall would do better β€” but adds a model dependency and a cache to + manage. +- **Verbatim caps are line-based, not token-based.** An 80-line + `context.md` can be either 400 or 4,000 tokens depending on how prose-y + it is. A runaway operator who pastes a long Markdown table fits inside + the cap but blows the prompt budget. A token-aware cap (using + `tiktoken` or the provider's tokenizer) would be sharper. +- **Write-time dedup is best-effort.** 15s timeout + 1 attempt means a + slow Anthropic API moment silently falls back to exact-string dedup. No + warning to the operator β€” the only visible signal is that paraphrased + duplicates show up in `learnings.md` until the next periodic compaction. +- **Anti-thrash is a heuristic.** 10% growth is reasonable but arbitrary; + a project that adds 9 lines/day for weeks never triggers compaction + growth-wise even though the file accumulates 60+ lines/week of small + drift. The target-distance fallback catches some of this once the file + is far enough from `max_lines`, but there's a dead zone. +- **`context.md` / `priorities.md` are read every mission.** No caching, + no change detection β€” every skill invocation re-reads them. Fine today + (small files, local disk) but could matter at scale or on NFS-backed + KOAN_ROOTs. +- **Three sources concatenated unconditionally.** If `context.md` and + `priorities.md` are both filled in and `K` learnings get past the + Jaccard filter, the resulting block can run 120+ lines, all in the + user prompt every mission. There's no overall budget β€” just per-source + caps that don't compose. +- **`build_memory_block_for_skill` returns `""` when `KOAN_ROOT` is + unset.** That's correct for standalone skill invocations outside an + instance, but it means a Kōan instance with a broken/missing env var + silently strips memory from every skill prompt β€” no warning logged. +- **Reverse-resolution failures fall back to basename silently.** If + `projects.yaml` is malformed and the lookup raises, the helper warns + and uses the directory basename β€” an operator who renamed only one + side won't notice the memory has gone dark. +- **Themed compaction sections aren't validated.** The prompt asks the + model to emit only the four themed sections plus `## Other`, but + there's no parser that enforces this. A model that invents extra + sections produces a file the human still has to read carefully. + +--- + +## 5. Risks + +- **Prompt-injection surface widened.** `context.md` is now injected + verbatim into every skill prompt. An operator who pastes untrusted + content into it (e.g. a copy-paste from a GitHub issue body that + contained a prompt-injection payload) hands that content to Claude + with no fencing. Mitigation today: the file is explicitly documented + as human-only territory. Future hardening: wrap the verbatim sections + in the same fencing applied by `prompt_guard.fence_external_data`. +- **Memory-block growth can starve the rest of the prompt.** The + agent-loop prompt already carries merge policy, PR guidelines, drift + detection, deep research, etc. Adding 80 + 40 + ~25 lines of memory + on top β€” every mission β€” eats into the prompt budget that future + features will want. +- **Reverse-lookup against `projects.yaml` runs on every skill call.** + Cheap today, but it does a `Path.resolve()` per configured project, + which can stat the filesystem and follow symlinks. On instances with + many projects on a slow or networked FS, this could become measurable. +- **Write-time dedup uses the lightweight model on the hot path.** Each + PR-review-learning cycle spawns a Claude CLI call. If lightweight is + rate-limited or down, the 15s timeout fires per cycle, slowing the + loop. The fallback works, but the operator only finds out by reading + stderr. +- **Legacy compact-state files are tolerated but never warned about.** + A plain-hash state file means anti-thrash falls back to the weaker + target-distance heuristic until the first successful compaction + rewrites it. Silently degraded behavior. +- **Empty-section enforcement in the compaction prompt is advisory.** + If the model emits `## Architecture notes\n\n## Gotchas\n- ...`, + the empty header survives into `learnings.md` and pollutes future + Jaccard scoring with section header tokens. +- **No upper bound on total memory size injected.** Three caps but no + global budget. An operator who increases `max_relevant_learnings` to + 200 and fills `context.md` to the brim can produce a 320-line + memory block. Skill prompts have no detection / clamp for this. + +--- + +## 6. Improvement axes + +### Near-term, low-cost + +- **Log a warning when `KOAN_ROOT` is unset inside an instance context.** + Distinguish "skill invoked standalone" (silent fallback is correct) + from "skill invoked by Kōan but env is broken" (should yell). +- **Emit a structured stat on write-time dedup outcome.** Surface + `{lessons_in, exact_dropped, semantic_dropped, lessons_appended, + cli_failed}` to the journal so an operator can see whether the + dedup pass is actually doing anything. +- **Token-aware caps for `context.md` / `priorities.md`.** Replace the + 80-line / 40-line caps with token caps using the provider's tokenizer + (we already have `cli_provider` indirection for the model name). +- **Fence the verbatim sections.** Apply `prompt_guard.fence_external_data` + to `context.md` and `priorities.md` content so an accidental + prompt-injection payload is at least neutralized. +- **Cap the total memory block.** Add a `memory.max_block_lines` config + knob that clamps the assembled block β€” drop learnings first + (least-confident source), then truncate context, then priorities. +- **Telemetry on anti-thrash skips.** Increment a counter in the + journal: how often does the guard fire vs. let through? Tunes the + 10% threshold from data. +- **Validate themed compaction output.** Parse the model's output; + drop empty sections; warn when the model invents headers outside + the allowed five. + +### Medium-term + +- **Embedding-based learnings recall.** Replace Jaccard with a small + embedding model (e.g. `text-embedding-3-small` via the provider, or + a local `sentence-transformers` model). Cache embeddings in + `instance/.koan-embeddings.jsonl` and invalidate on + `learnings.md` hash change. Bigger relevance lift than any cap-tuning. +- **Per-skill memory budget overrides.** `/review` may want more + diff-related learnings; `/plan` may want more architecture context. + Add `memory:` sub-keys per skill in `config.yaml`. +- **Memory-block diff in journal.** When the assembled block changes + meaningfully between consecutive missions on the same project, log + what was added/dropped β€” gives the operator visibility into "why + did Claude suddenly know about X." +- **Combine compaction reasons.** Today anti-thrash is binary skip / + run. A "lazy compaction" mode could merge only the section that + grew (e.g. only `## Gotchas` if all new lessons landed there), which + would defeat the 10% threshold without burning the full pass. +- **Cache `context.md` / `priorities.md` reads per process.** Re-read + only on mtime change. Saves I/O for the worst-case future + high-frequency skill dispatch. + +### Long-term / structural + +- **Promote memory to a first-class store.** Today everything is Markdown + files. A SQLite store would let us index by embedding, tag by source, + expire by age, and surface metrics β€” without losing the human-readable + fallback (export to MD on demand). +- **Bidirectional curation.** Let the agent *propose* edits to + `context.md` / `priorities.md` (e.g. "I noticed this project moved its + CLI from Click to Typer β€” update context.md?") that the human accepts + or rejects via Telegram. Today those files are strictly human-write. +- **Cross-project lessons.** A pattern learned on project A ("always run + `make lint` before claiming a fix is done") often applies to project + B. A `memory/global/cross-project-learnings.md` injected alongside + the per-project block would close that gap. Needs a curation step to + decide what's truly portable. +- **Replace the two-pass dedup with a streaming agent.** The current + write-time dedup is "filter candidates against existing." A small + agent that *merges* the new candidate into an existing entry + ("update the threshold from 3 to 5") would be more useful than just + dropping near-duplicates. + +--- + +## 7. Files touched + +| File | Change | +|------|--------| +| `koan/app/skill_memory.py` | **New** β€” shared helper module | +| `koan/app/prompt_builder.py` | Delegate `_get_learnings_section` to helper | +| `koan/app/plan_runner.py` | Inject memory in 3 paths (review loop, new, iterate) | +| `koan/app/review_runner.py` | Inject memory; score against title+body+diff | +| `koan/skills/core/fix/fix_runner.py` | Inject memory | +| `koan/skills/core/implement/implement_runner.py` | Inject memory | +| `koan/skills/core/{fix,implement,plan,review}/prompts/*.md` | Add `{PROJECT_MEMORY}` placeholder | +| `koan/app/memory_manager.py` | Anti-thrash guard + JSON state file | +| `koan/app/pr_review_learning.py` | Write-time semantic dedup | +| `koan/system-prompts/learnings-dedup.md` | **New** β€” dedup prompt | +| `koan/system-prompts/learnings-compaction.md` | Themed output sections | +| `koan/system-prompts/agent.md` | Document the three memory sources | +| `instance.example/config.yaml` | Document `memory.write_time_dedup` | +| `instance.example/memory/projects/_template/context.md` | Document auto-injection | +| `instance.example/memory/projects/_template/priorities.md` | Document auto-injection | + +Tests: 21 new (`test_skill_memory`: 12, `test_pr_review_learning` dedup: 5, +`test_memory_manager` anti-thrash + state: 4). Full suite: 12,772 pass. 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/provider-codex.md b/docs/provider-codex.md index 6eeed767d..010b84dea 100644 --- a/docs/provider-codex.md +++ b/docs/provider-codex.md @@ -76,7 +76,7 @@ Available models (as of March 2026): Kōan invokes Codex in **non-interactive mode** via `codex exec`: ``` -codex --full-auto --model gpt-5.4 exec "Your prompt here" +codex exec --sandbox workspace-write --model gpt-5.4 "Your prompt here" ``` This runs Codex as a scripted agent that reads the project, generates @@ -86,7 +86,7 @@ a plan, executes it, and streams the result to stdout. | Kōan Setting | Codex Flag | Behavior | |-----------------------|------------------|---------------------------------| -| `skip_permissions: false` | `--full-auto` | Workspace writes + on-request approvals | +| `skip_permissions: false` | `--sandbox workspace-write` | Workspace writes + on-request approvals | | `skip_permissions: true` | `--yolo` | No approvals, no sandbox | ### Feature Mapping @@ -159,8 +159,8 @@ Kōan's quota detection will pause and notify you. Codex does not support per-tool allow/disallow flags. Tool access is controlled by sandbox policies. Use `skip_permissions: true` (maps to -`--yolo`) for full access, or the default `--full-auto` for workspace- -scoped writes. +`--yolo`) for full access, or the default `--sandbox workspace-write` +for workspace-scoped writes. ### System prompt not taking effect 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/security-review.md b/docs/security-review.md new file mode 100644 index 000000000..451ca5b70 --- /dev/null +++ b/docs/security-review.md @@ -0,0 +1,136 @@ +# Security Review + +Kōan can automatically scan mission diffs for security-sensitive patterns before auto-merge. This provides a lightweight safety net that catches common dangerous code patterns without requiring an external tool. + +## Overview + +When enabled, the security review runs as part of the post-mission pipeline, between reflection and auto-merge. It: + +1. **Calculates blast radius** β€” files changed, modules affected, infrastructure/dependency changes +2. **Scans content patterns** β€” eval/exec, shell injection, hardcoded secrets, unsafe deserialization, XSS, wildcard CORS, and more +3. **Classifies risk** β€” low / medium / high / critical based on cumulative score +4. **Logs to journal** β€” all findings are recorded in the project's daily journal +5. **Optionally blocks auto-merge** β€” when configured in blocking mode with a severity threshold + +The review is designed to be fail-open: if it encounters an error (git failure, config issue), auto-merge proceeds normally. + +## Configuration + +Security review is configured per-project in `projects.yaml`. See `projects.example.yaml` for a full annotated example. + +### Basic setup + +```yaml +defaults: + security_review: + enabled: true # Scan diffs for dangerous patterns + blocking: false # Log findings but don't block auto-merge + severity_threshold: high # Threshold for blocking (when blocking: true) +``` + +### Blocking mode + +When `blocking: true`, auto-merge is skipped if the risk level meets or exceeds `severity_threshold`: + +```yaml +defaults: + security_review: + enabled: true + blocking: true # Block auto-merge on risky changes + severity_threshold: medium # Block on medium, high, or critical risk +``` + +### Per-project overrides + +Override the defaults for specific projects: + +```yaml +projects: + production-api: + security_review: + enabled: true + blocking: true # Strict: block on risky changes + severity_threshold: medium + + internal-tool: + security_review: + enabled: false # Skip review for low-risk internal tools +``` + +### Options + +| Setting | Default | Description | +|---|---|---| +| `enabled` | `false` | Run the security review on every mission. | +| `blocking` | `false` | Block auto-merge when risk meets the threshold. When false, findings are logged but auto-merge proceeds. | +| `severity_threshold` | `high` | Minimum risk level that triggers a block (when `blocking: true`). One of: `low`, `medium`, `high`, `critical`. | + +## What It Detects + +### Content patterns (added lines only) + +The review scans only added lines in the diff (`+` lines), ignoring removed code: + +- **`eval()` / `exec()`** β€” dynamic code execution +- **`subprocess` with `shell=True`** β€” shell injection risk +- **`os.system()`** β€” shell command execution +- **SQL string formatting** β€” potential SQL injection +- **Hardcoded secrets** β€” `api_key = "..."`, `password = "..."` +- **SSL/TLS verification disabled** β€” `disable_ssl`, `verify=False` +- **Overly permissive permissions** β€” `chmod 777`, `chmod 666` +- **Verification bypass** β€” `--no-verify` flags +- **Wildcard CORS** β€” `Access-Control-Allow-Origin: *` +- **Unsafe deserialization** β€” `pickle.load()`, `marshal.load()` +- **XSS vectors** β€” `.innerHTML =`, `dangerouslySetInnerHTML` + +### Blast radius factors + +- Number of files changed (>5, >10, >20 files increase risk) +- Sensitive file paths (secrets, credentials, auth, tokens, configs) +- Infrastructure files (Dockerfile, docker-compose, Makefile) +- Dependency files (requirements.txt, package.json, pyproject.toml, etc.) +- Number of top-level modules affected + +## Risk Scoring + +The risk level is calculated from a cumulative score: + +| Risk Level | Score Threshold | +|---|---| +| Low | 0+ | +| Medium | 6+ | +| High | 12+ | +| Critical | 20+ | + +Points are awarded for: +- File count: 1 (>5 files), 2 (>10), 4 (>20) +- Each sensitive file: 3 points +- Infrastructure changes: 3 points +- Dependency changes: 2 points +- Multiple modules: 1 (>1 module), 2 (>3 modules) +- Each content finding: 2 points + +## Journal Output + +Review results are written to the project's daily journal (`instance/journal/YYYY-MM-DD/project.md`): + +```markdown +## Security Review β€” risk: medium (score: 8) +- Files: 7, Sensitive: 1, Modules: 2 +- ⚠ Dependency changes detected +- Content findings (2): + - eval() usage: `result = eval(user_input)` + - hardcoded secret: `api_key = "sk-live-..."` +- **Auto-merge blocked** by security review +``` + +## Pipeline Integration + +The security review runs in the post-mission pipeline in `mission_runner.py`: + +1. Verification (quality gate, lint) +2. Reflection +3. **Security review** ← here +4. Auto-merge (skipped if security review blocks) + +If the review itself fails (exception), it logs the error and returns "pass" to avoid blocking the pipeline on review infrastructure issues. diff --git a/docs/skills.md b/docs/skills.md index 303a65099..c086ca285 100644 --- a/docs/skills.md +++ b/docs/skills.md @@ -43,6 +43,7 @@ Complete reference for all Koan slash commands. Use these via Telegram, Slack, o | `/check ` | `/inspect` | Run project health checks (rebase, review, plan) | β€” | | `/pr ` | β€” | Review and update a GitHub pull request | β€” | | `/claudemd [project]` | `/claude`, `/claude.md` | Refresh or create a project's CLAUDE.md | β€” | +| `/doc [cats]` | `/docs` | Extract structured documentation to docs/ | Yes | Skills marked **GitHub @mention** can be triggered by commenting `@koan-bot ` on a PR or issue. See [github-commands.md](github-commands.md). @@ -86,6 +87,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 +111,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..f178ffc49 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,48 @@ 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 +
+ +**`/inbox`** β€” Force a GitHub notification check and show how many GitHub-originated missions are queued. + +
+Use cases + +- `/inbox` β€” Quick check: "do I have GitHub mail?" β€” triggers a fetch and shows pending πŸ“¬ mission count
**`/verbose`** / **`/silent`** β€” Toggle real-time progress updates. When verbose is on, Kōan sends progress messages as it works. @@ -253,6 +284,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 +349,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