Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .branch-cleanup-tracker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"": {
"last_cleanup_ts": 1780826647.3286903
}
}
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Communication between processes happens through shared files in `instance/` with
- **`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
- **`sdlc_state.py`** — SDLC workflow state persistence: `SdlcPhase` enum, `SdlcState` dataclass, workspace helpers (`get_sdlc_workspace`, `load_sdlc_state`, `save_sdlc_state`, `get_artifact_path`, `archive_sdlc_workspace`). Stores per-run state and phase artifacts under `instance/sdlc/{issue_name}/`. Foundation for the `/sdlc` orchestrator skill.
- **`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
Expand All @@ -160,7 +161,7 @@ Config additions in `config.py`: `is_api_enabled()`, `get_api_host()` (default `
Extensible command plugin system. Each skill lives in `skills/<scope>/<skill-name>/` 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/` (abort, add_project, ai, alias, ask, audit, autoreview, brainstorm, branches, cancel, changelog, chat, check, check_need, check_notifications, checkup, ci_check, claudemd, config_check, dead_code, deep, deepplan, delete_project, diagnose, doc, doctor, done, email, explain, explore, fix, focus, gh_request, gha_audit, idea, implement, inbox, incident, journal, language, list, live, logs, magic, mission, models, passive, plan, plan_implement, pr, priority, private_security_audit, profile, projects, quota, rebase, recreate, recurring, refactor, reflect, rename, rescan, reset, restart, review, review_rebase, rtk, scaffold_skill, security_audit, shutdown, snapshot, sparring, spec_audit, squash, stats, status, tech_debt, tracker, verbose, version)
- **Core skills** live in `koan/skills/core/` (abort, add_project, ai, alias, ask, audit, autoreview, brainstorm, branches, cancel, changelog, chat, check, check_need, check_notifications, checkup, ci_check, claudemd, config_check, dead_code, deep, deepplan, delete_project, diagnose, doc, doctor, done, email, explain, explore, fix, focus, gh_request, gha_audit, idea, implement, inbox, incident, journal, language, list, live, logs, magic, mission, models, passive, plan, plan_implement, pr, priority, private_security_audit, profile, projects, quota, rebase, recreate, recurring, refactor, reflect, rename, rescan, reset, restart, review, review_rebase, rtk, scaffold_skill, sdlc, security_audit, shutdown, snapshot, sparring, spec_audit, squash, stats, status, tech_debt, tracker, verbose, version)
- **Custom skills** loaded from `instance/skills/<scope>/` — 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.
Expand All @@ -180,6 +181,7 @@ Extensible command plugin system. Each skill lives in `skills/<scope>/<skill-nam
- `journal/` — Daily logs organized as `YYYY-MM-DD/project.md`
- `events/` — One-shot scheduled missions (JSON files consumed by `event_scheduler.py`)
- `hooks/` — User-defined Python hook modules for lifecycle events (see `instance.example/hooks/README.md`)
- `sdlc/` — Per-workflow SDLC state: `sdlc/{issue_name}/STATE.json` + phase artifact files (RESEARCH.md, ADR.md, PLAN.md, etc.). Managed by `sdlc_state.py`. Archived to `sdlc/_archived/` on terminal phase (PRODUCTION_READY or ABANDONED). Never logged verbatim — may contain sensitive project details.

## Python compatibility

Expand Down
3 changes: 3 additions & 0 deletions docs/users/skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ Skills marked **GitHub @mention** can be triggered by commenting `@koan-bot <com
| `/scaffold_skill <scope> <name> <desc>` | `/scaffold`, `/new_skill` | Generate SKILL.md + handler.py for a new custom skill |
| `/rtk [setup\|uninstall\|gain\|on\|off]` | — | Manage optional rtk integration for compressed tool output |
| `/ideas` | — | List all ideas in the backlog |
| `/sdlc <issue-name> [description]` | — | Start or resume a multi-phase SDLC workflow (Research → Architecture → Planning → [approval] → Implementation → Review → Documentation → Production Ready) |
| `/approve <issue-name>` | — | Approve an SDLC plan and start implementation |
| `/reject <issue-name>` | — | Reject an SDLC plan and abandon the workflow |

---

Expand Down
20 changes: 20 additions & 0 deletions docs/users/user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -1860,6 +1860,23 @@ projects:
- `/psecu webapp focus on token handling limit=3` — Focused review, kept off GitHub
</details>

### SDLC Orchestrator

**`/sdlc`** — Run a full software development lifecycle for a GitHub issue. Kōan works through phases sequentially (Research → Architecture → Planning → [human approval] → Implementation → Review → Documentation → Production Ready), pausing for your sign-off before writing any code.

- **Usage:** `/sdlc <issue-name> ["description"]`
- **Flags:** `--resume` (re-queue current phase), `--plan` (jump to planning), `--implement` (jump to implementation), `--review` (jump to review), `--approve` (approve plan and start implementation)
- **Approval flow:** When planning completes, Kōan posts the plan and waits. Reply `/approve <issue-name>` to proceed or `/reject <issue-name>` to abandon.

<details>
<summary>Use cases</summary>

- `/sdlc add-oauth2 "Add OAuth2 login support"` — Start a full SDLC workflow for a new feature
- `/sdlc add-oauth2 --resume` — Re-queue the current phase if it was skipped
- `/approve add-oauth2` — Approve the plan and start implementation
- `/reject add-oauth2` — Abandon the workflow
</details>

### Incident Triage

**`/incident`** — Triage a production error from a stack trace or log snippet. Kōan will parse the error, identify the root cause, propose a fix with tests, and submit a draft PR.
Expand Down Expand Up @@ -1997,6 +2014,9 @@ All commands at a glance. **Tier:** B = Beginner, I = Intermediate, P = Power Us
| `/tech_debt [project]` | `/td`, `/debt` | P | Scan project for tech debt |
| `/dead_code [project]` | `/dc` | P | Scan for unused code |
| `/spec_audit [project]` | `/sa`, `/drift` | P | Audit docs/code alignment, queue fix missions |
| `/sdlc <issue-name> [description]` | — | P | Run full SDLC workflow (Research→Architecture→Planning→[approval]→Implementation→Review→Docs) |
| `/approve <issue-name>` | — | P | Approve SDLC plan and start implementation |
| `/reject <issue-name>` | — | P | Reject SDLC plan and abandon workflow |
| `/incident <error>` | — | P | Triage a production error |
| `/scaffold_skill <scope> <name> <desc>` | `/scaffold`, `/new_skill` | P | Generate SKILL.md + handler.py for a new custom skill |
| `/rtk [setup\|uninstall\|gain\|on\|off]` | — | P | Manage optional [rtk](https://github.com/rtk-ai/rtk) integration for compressed tool output (60-90 % token savings on Bash commands). See [docs/operations/rtk.md](../operations/rtk.md). |
Expand Down
2 changes: 1 addition & 1 deletion koan/.branch-cleanup-tracker.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"": {
"last_cleanup_ts": 1780710677.913166
"last_cleanup_ts": 1780825787.1351092
}
}
129 changes: 123 additions & 6 deletions koan/app/command_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def set_callbacks(
"help", "stop", "update", "upgrade", "sleep", "resume", "skill",
"pause", "work", "awake", "start", "run", # aliases for sleep/resume
"at", # one-shot scheduled mission trigger
"approve", "reject", # SDLC workflow approval gate
})


Expand Down Expand Up @@ -145,6 +146,16 @@ def handle_command(text: str):
_handle_start()
return

if cmd == "/approve" or cmd.startswith("/approve "):
issue = text.strip().split(None, 1)[1].strip() if " " in text.strip() else ""
_handle_sdlc_approve(issue)
return

if cmd == "/reject" or cmd.startswith("/reject "):
issue = text.strip().split(None, 1)[1].strip() if " " in text.strip() else ""
_handle_sdlc_reject(issue)
return

if cmd == "/help" or cmd.startswith("/help "):
help_args = text.strip()[5:].strip() # everything after "/help"
if help_args:
Expand Down Expand Up @@ -581,12 +592,14 @@ def _handle_skill_approve(args: str):
# Core commands that are hardcoded (not skill-based) but should appear in /help.
# Each entry: (command_name, description, aliases, group)
_CORE_COMMAND_HELP = [
("help", "Show help overview or details", ["h"], "system"),
("stop", "Stop the run loop", [], "system"),
("update", "Finish current mission, update, restart", ["upgrade"], "system"),
("pause", "Pause mission processing (optional: /pause 2h)", ["sleep"], "system"),
("resume", "Resume mission processing", ["work", "awake", "run", "start"], "system"),
("skill", "Manage skill packages", [], "system"),
("help", "Show help overview or details", ["h"], "system"),
("stop", "Stop the run loop", [], "system"),
("update", "Finish current mission, update, restart", ["upgrade"], "system"),
("pause", "Pause mission processing (optional: /pause 2h)", ["sleep"], "system"),
("resume", "Resume mission processing", ["work", "awake", "run", "start"], "system"),
("skill", "Manage skill packages", [], "system"),
("approve", "Approve an SDLC plan and start implementation (e.g. /approve my-feature)", [], "missions"),
("reject", "Abandon an SDLC workflow awaiting approval (e.g. /reject my-feature)", [], "missions"),
]

# Ordered group list (controls display order in /help)
Expand Down Expand Up @@ -872,6 +885,110 @@ def handle_resume():
send_telegram("⚠️ Error checking quota. /status or check manually.")


def _handle_sdlc_approve(issue_name: str) -> None:
"""Handle /approve <issue-name> — approve an SDLC plan and start implementation."""
if not issue_name:
send_telegram("Usage: /approve <issue-name>")
return

from app.sdlc_state import SdlcPhase, load_sdlc_state, save_sdlc_state

state = load_sdlc_state(str(INSTANCE_DIR), issue_name)
if state is None:
send_telegram(f"⚠️ No SDLC workflow found for `{issue_name}`.")
return
if state.current_phase != SdlcPhase.AWAITING_APPROVAL:
send_telegram(
f"⚠️ `{issue_name}` is not awaiting approval "
f"(current phase: {state.current_phase.value})."
)
return

state.approved = True
state.current_phase = SdlcPhase.IMPLEMENTATION
save_sdlc_state(str(INSTANCE_DIR), state)

project_name = _extract_project_from_sentinel(issue_name)
_remove_sdlc_sentinel(issue_name)
_queue_sdlc_phase(issue_name, project_name)

send_telegram(f"✅ `{issue_name}` approved — implementation queued.")


def _handle_sdlc_reject(issue_name: str) -> None:
"""Handle /reject <issue-name> — abandon an SDLC workflow awaiting approval."""
if not issue_name:
send_telegram("Usage: /reject <issue-name>")
return

from app.sdlc_state import SdlcPhase, load_sdlc_state, save_sdlc_state
from app.sdlc_state import archive_sdlc_workspace

state = load_sdlc_state(str(INSTANCE_DIR), issue_name)
if state is None:
send_telegram(f"⚠️ No SDLC workflow found for `{issue_name}`.")
return
if state.current_phase != SdlcPhase.AWAITING_APPROVAL:
send_telegram(
f"⚠️ `{issue_name}` is not awaiting approval "
f"(current phase: {state.current_phase.value})."
)
return

state.current_phase = SdlcPhase.ABANDONED
save_sdlc_state(str(INSTANCE_DIR), state)
_remove_sdlc_sentinel(issue_name)
archive_sdlc_workspace(str(INSTANCE_DIR), issue_name)
send_telegram(f"🗑️ `{issue_name}` rejected and archived.")


def _extract_project_from_sentinel(issue_name: str) -> str:
"""Extract the project name from an existing sdlc:awaiting-approval sentinel."""
from app.utils import PROJECT_TAG_RE

tag = f"[sdlc:awaiting-approval:{issue_name}]"
try:
content = MISSIONS_FILE.read_text(encoding="utf-8")
for line in content.splitlines():
if tag in line:
m = PROJECT_TAG_RE.search(line)
if m:
return m.group(1)
except OSError:
pass
try:
projects = get_known_projects(str(KOAN_ROOT))
if projects:
return projects[0]
except Exception as e:
import sys
print(f"[command_handlers] _resolve_approval_project: {e}", file=sys.stderr)
return ""


def _remove_sdlc_sentinel(issue_name: str) -> None:
"""Remove [sdlc:awaiting-approval:{issue_name}] sentinel from missions.md."""
tag = f"[sdlc:awaiting-approval:{issue_name}]"
try:
content = MISSIONS_FILE.read_text(encoding="utf-8")
filtered = "\n".join(ln for ln in content.splitlines() if tag not in ln)
if filtered.rstrip("\n") != content.rstrip("\n"):
atomic_write(MISSIONS_FILE, filtered + "\n")
except (OSError, FileNotFoundError):
pass


def _queue_sdlc_phase(issue_name: str, project_name: str) -> None:
"""Queue /sdlc_phase <issue_name> into missions.md."""
from app.missions import insert_mission

missions_content = MISSIONS_FILE.read_text(encoding="utf-8") if MISSIONS_FILE.exists() else ""
project_tag = f"[project:{project_name}] " if project_name else ""
entry = f"{project_tag}/sdlc_phase {issue_name}"
updated = insert_mission(missions_content, entry, urgent=False)
atomic_write(MISSIONS_FILE, updated)


def _handle_start():
"""Start the agent loop — smart command that handles both cases.

Expand Down
23 changes: 23 additions & 0 deletions koan/app/iteration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,29 @@ def plan_iteration(
else:
_log_iteration("koan", "No pending mission — entering autonomous mode")

# Step 4b-sdlc: SDLC approval gate — sentinel missions must never execute.
# The human must send /approve <issue-name> to advance.
if mission_project and mission_title and "[sdlc:awaiting-approval:" in mission_title:
_log_iteration(
"koan",
f"SDLC awaiting approval for '{mission_title}' — skipping execution",
)
return _make_result(
action="sdlc_wait",
project_name=mission_project,
project_path="",
mission_title=mission_title,
autonomous_mode=autonomous_mode,
focus_area="Waiting for SDLC approval — reply /approve <issue-name>",
available_pct=available_pct,
decision_reason="SDLC approval sentinel — waiting for /approve",
display_lines=display_lines,
recurring_injected=recurring_injected,
focus_remaining=None,
schedule_mode=schedule_state.mode if schedule_state else "normal",
tracker_error=tracker_error,
)

# Step 4b: Passive mode gate — block all execution
# Missions stay Pending, no autonomous work. Must check before start_mission().
passive_state = _check_passive(koan_root)
Expand Down
4 changes: 4 additions & 0 deletions koan/app/mission_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,10 @@ def _run_iteration(
p.get("decision_reason") or "Project branch-saturated — waiting for reviews/merges",
f"Branch-saturated — waiting ({time.strftime('%H:%M')})",
),
"sdlc_wait": lambda p: (
p.get("decision_reason") or "SDLC awaiting human approval",
f"SDLC approval pending — /approve <issue-name> to proceed ({time.strftime('%H:%M')})",
),
}
if action in _IDLE_WAIT_CONFIG:
log_msg, status_msg = _IDLE_WAIT_CONFIG[action](plan)
Expand Down
1 change: 1 addition & 0 deletions koan/app/missions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,7 @@ def update_ci_item_attempt(content: str, pr_url: str) -> str:
r"|deeplan|deepplan|doc|docs|doit|explain|fix|gh_request"
r"|impl|implement|inspect|need|needs|perf|plan|plandoit|planimp|planimplement|planimpl|planit|profile"
r"|rb|rc|rebase|recreate|refactor|review|reviewrebase|rf|rr|rv|xp"
r"|sdlc|sdlc_run"
r"|secu|security|security_audit|sq|squash)\s+"
r"(https://github\.com/[^\s]+)"
)
Expand Down
Loading
Loading