loki-mode 11.3.8 → 11.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -18
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/agents/custom_agents.py +280 -0
- package/autonomy/council-v2.sh +22 -1
- package/autonomy/edit.sh +242 -0
- package/autonomy/lib/chat.sh +312 -0
- package/autonomy/lib/help-core.sh +164 -0
- package/autonomy/lib/json-output.sh +38 -0
- package/autonomy/lib/run-summary.sh +96 -0
- package/autonomy/loki +105 -10
- package/autonomy/route.sh +270 -0
- package/autonomy/run.sh +6 -0
- package/autonomy/setup-wizard.sh +161 -0
- package/autonomy/verify.sh +22 -2
- package/bin/loki +73 -18
- package/completions/_loki +55 -0
- package/completions/loki.bash +49 -3
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-CORE-VERBS.md +112 -0
- package/docs/CLI-REFERENCE.md +7 -1
- package/docs/COMPARE-OPENCODE.md +94 -0
- package/docs/COMPONENTS.md +1 -0
- package/docs/INSTALLATION.md +56 -12
- package/docs/KNOWN-ISSUES.md +2 -12
- package/docs/QUICKSTART.md +108 -0
- package/docs/environment-variables.md +9 -2
- package/docs/exit-codes.md +29 -12
- package/docs/v10/CLI-MODERN.md +10 -4
- package/docs/v10/FAILURE-CLASSES.md +2 -2
- package/docs/v10/GUIDE.md +270 -44
- package/docs/v10/METRICS.md +6 -0
- package/docs/v10/OPENCODE-PARITY-PLAN.md +84 -0
- package/docs/v10/PROGRESS.md +7 -0
- package/loki-ts/dist/loki.js +968 -933
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +198 -86
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +16 -2
- package/providers/model_route_exec.sh +22 -0
- package/providers/model_routes.sh +188 -0
- package/skills/providers.md +13 -0
- package/templates/agents/planner.md +10 -0
- package/templates/agents/reviewer.md +10 -0
- package/templates/agents/tester.md +10 -0
- package/web-app/dist/assets/{AdminPage-D1jlQGeE.js → AdminPage-CYJ2BWRC.js} +1 -1
- package/web-app/dist/assets/{Avatar-QUX_aJfQ.js → Avatar-DdK4wynI.js} +1 -1
- package/web-app/dist/assets/{Badge-pysXUR6J.js → Badge-CrX3MI7J.js} +1 -1
- package/web-app/dist/assets/{Button-CWzlTw9P.js → Button-709-wwGM.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DLCoLdyF.js → CockpitPage-BapdTInO.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CCENGIti.js → ComparePage-DT99pGvA.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BssCcbgv.js → ErrorBoundary-B0uJSys8.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CwmAqJ90.js → EvidenceReceiptPanel-BtMFgElP.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-DuSw8shb.js → GitHubIssuesPanel-DjJuU941.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CGvLbDjh.js → GitHubPRsPanel-HXfEX1BC.js} +1 -1
- package/web-app/dist/assets/{HomePage-BrBX5Gn2.js → HomePage-Cqyves-S.js} +2 -2
- package/web-app/dist/assets/{LoginPage-DPjr7QHU.js → LoginPage-jUbtC7_Y.js} +1 -1
- package/web-app/dist/assets/{MagicPage-Dxd7FWF2.js → MagicPage-DHRsqjgR.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-BP47Urni.js → MetricsPage-B-pED05R.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-D7_07YoB.js → NotFoundPage-CsbUs21R.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-SQ183efy.js → ProjectPage-BtlLEQwB.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-Cgr1K2Ae.js → ProjectsPage-DaRbRjSp.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-kyYOSDt0.js → SettingsPage-DALrtP3F.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CRd-H1A4.js → ShowcasePage-CoPHSNP0.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BHBdGXod.js → SystemSettingsPage-kOuSN50O.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-CI3Wdbjw.js → TeamsPage-DGWK0LMp.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-DfhCgP6i.js → TemplatesPage-C-_10CwA.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-Cs87YXLJ.js → TerminalOutput-Cz_vPbXL.js} +1 -1
- package/web-app/dist/assets/{activity-CrQx5SbK.js → activity-icIsXJVa.js} +1 -1
- package/web-app/dist/assets/{bell-Dde5IbnI.js → bell-DbPR2_4r.js} +1 -1
- package/web-app/dist/assets/{bot-trRuIJT6.js → bot-DvjP3Yh5.js} +1 -1
- package/web-app/dist/assets/{check-DmsthR-3.js → check-D1OBxHeU.js} +1 -1
- package/web-app/dist/assets/{chevron-left-C5wZb7mg.js → chevron-left-D09jDZ29.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BCb173fK.js → circle-alert-DyZMLOcC.js} +1 -1
- package/web-app/dist/assets/{clock-CvcqL3Ma.js → clock-CdpdJTId.js} +1 -1
- package/web-app/dist/assets/{cloud-CW1r3-AN.js → cloud-CoLTlmoj.js} +1 -1
- package/web-app/dist/assets/{code-xml-uxBTXAfZ.js → code-xml-DrkE2ZZY.js} +1 -1
- package/web-app/dist/assets/{database-DBrvW0o1.js → database-CMQQuM2R.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-WrOyoDcN.js → dollar-sign-CuioDaOG.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-8JC8TI2_.js → file-code-corner-Cx9j9K1S.js} +1 -1
- package/web-app/dist/assets/{file-plus-CgUdOr2u.js → file-plus-CYD2zaaI.js} +1 -1
- package/web-app/dist/assets/{globe-w-0x46nt.js → globe-6w-hnQkL.js} +1 -1
- package/web-app/dist/assets/{hammer-Civ6_wQq.js → hammer-BpqtautL.js} +1 -1
- package/web-app/dist/assets/{index-wvGJotyF.js → index-Dzs-9EgJ.js} +3 -3
- package/web-app/dist/assets/{layers-Cc3wLG39.js → layers-D5hd7jb7.js} +1 -1
- package/web-app/dist/assets/{loader-circle-BedHqfS4.js → loader-circle-C6Y9x_7H.js} +1 -1
- package/web-app/dist/assets/{lock-CRyzk3QG.js → lock-D51L7rYh.js} +1 -1
- package/web-app/dist/assets/{package-DCT1v7W2.js → package-BJF8hkP8.js} +1 -1
- package/web-app/dist/assets/{plus-C7eerOb_.js → plus-D5kUQ79n.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-DO753Y_B.js → refresh-cw-BKdlCa7F.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-ptlyGV4I.js → rotate-ccw-_bd_YI1e.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CqPwk4yI.js → scroll-text-DbkegIxF.js} +1 -1
- package/web-app/dist/assets/{server-BP_VvY9m.js → server-DAwqFIsq.js} +1 -1
- package/web-app/dist/assets/{shield-alert-Bf0waB0a.js → shield-alert-BuR1tPHu.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-p1roSIsw.js → thumbs-up-BF6p3mRd.js} +1 -1
- package/web-app/dist/assets/{trash-2-BrMaXAxl.js → trash-2-BVpnkqqz.js} +1 -1
- package/web-app/dist/assets/{trending-down-DaGcCn2W.js → trending-down-DpsDvRwW.js} +1 -1
- package/web-app/dist/assets/{trending-up-CKPupOSE.js → trending-up-mFogTjt3.js} +1 -1
- package/web-app/dist/assets/{upload-CR0gzibW.js → upload-4T8zpBsi.js} +1 -1
- package/web-app/dist/assets/{usePolling-kKzJRjFw.js → usePolling-DPXQv8WO.js} +1 -1
- package/web-app/dist/assets/{user-CPOXPTtD.js → user-BYGIFaTm.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Your agent says done. Loki proves it: an autonomous coding agent that turns a ta
|
|
|
7
7
|
[](https://hub.docker.com/r/asklokesh/loki-mode)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
[Website](https://www.autonomi.dev/) | [Install guide](docs/INSTALLATION.md) | [User guide](docs/v10/GUIDE.md) | [CLI reference](docs/CLI-
|
|
10
|
+
[Website](https://www.autonomi.dev/) | [Quickstart](docs/QUICKSTART.md) | [Install guide](docs/INSTALLATION.md) | [User guide](docs/v10/GUIDE.md) | [CLI reference](docs/CLI-CORE-VERBS.md) | [Compare with opencode](docs/COMPARE-OPENCODE.md) | [Changelog](CHANGELOG.md)
|
|
11
11
|
|
|
12
12
|
<!-- generated:facts -->
|
|
13
13
|
The npm badge above tracks the current release; see [CHANGELOG.md](CHANGELOG.md) for release notes.
|
|
@@ -31,18 +31,19 @@ npx loki-mode tour # prints a real receipt from a past build: no key, no spen
|
|
|
31
31
|
|
|
32
32
|
## First run
|
|
33
33
|
|
|
34
|
-
Loki needs a model.
|
|
34
|
+
Loki needs a model. [Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`) is the default provider: install it and log in. The bundled Agent SDK with an `ANTHROPIC_API_KEY` can stand in for the CLI on the main path. Other coding CLIs are optional. See [Requirements](#requirements).
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
export ANTHROPIC_API_KEY=sk-...
|
|
38
37
|
cd your-repo
|
|
39
|
-
loki
|
|
40
|
-
loki
|
|
41
|
-
loki
|
|
42
|
-
loki owner/repo#123
|
|
38
|
+
loki setup # first-run wizard (or: loki doctor --fix)
|
|
39
|
+
loki start "add rate limiting to /api/login" # the primary path: build, verify, seal a receipt, open a PR
|
|
40
|
+
loki start prd.md # same, from a PRD file
|
|
41
|
+
loki start owner/repo#123 # same, from a GitHub, GitLab or Jira issue
|
|
42
|
+
loki edit "rename fooBar to foo_bar" # one small change, no loop
|
|
43
|
+
loki chat # interactive alternative: Plan and Build modes
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
`loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run on the Loki 10 engine by default. <!-- loki10-default --> Add `--no-pr` to build and verify without opening a pull request, `--max-cost <usd>` to cap spend
|
|
46
|
+
`loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` all run on the Loki 10 engine by default, whatever the flag order or input form. <!-- loki10-default --> Add `--no-pr` to build and verify without opening a pull request, `--max-cost <usd>` to cap spend, and `--provider <name>` to pick another coding CLI. The spend cap defaults to $100 when `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` is set and to no dollar cap on a subscription login; `budgets.per_run` in `loki.yaml` also sets it. Inspect the route without running: `loki route start prd.md` or `--explain-route`. New here? The [5-minute quickstart](docs/QUICKSTART.md) walks through it.
|
|
46
47
|
|
|
47
48
|
## What you get
|
|
48
49
|
|
|
@@ -56,9 +57,9 @@ Every run starts from a delivery contract derived from your task, writes checks
|
|
|
56
57
|
loki verify # newest run; or: loki verify <run-id>
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
`loki verify` re-hashes the receipt, checks its signature and checks it against the run's event log. It prints
|
|
60
|
+
`loki verify` re-hashes the receipt, checks its signature and checks it against the run's event log. It prints a verdict and exits 0 (VERIFIED), 1 (CONCERNS), 2 (BLOCKED), 3 (verifier error, never a silent pass) or 64 (usage error); `[ $rc -eq 0 ]` is the only safe test for "verified". Full table: [docs/exit-codes.md](docs/exit-codes.md). On another machine, export the public key with `loki keys export` and pass it with `loki verify --pubkey <file>`. A receipt claims only what its checks ran, and lists what they did not; signing proves who produced it, not that the code is bug-free. Details: [docs/SIGNED-RECEIPTS.md](docs/SIGNED-RECEIPTS.md).
|
|
60
61
|
|
|
61
|
-
A finished run prints a short summary:
|
|
62
|
+
A finished run prints a short summary (every run on both engines also ends with a unified Result, Files, Cost, Elapsed, Receipt and `next:` block; `--quiet` prints only that block, `--json` adds a `summary` object):
|
|
62
63
|
|
|
63
64
|
```
|
|
64
65
|
Outcome: VERIFIED
|
|
@@ -73,19 +74,33 @@ Time: 4m12s (intake 11s, plan 20s, implement 3m10s, verify 31s)
|
|
|
73
74
|
|
|
74
75
|
| Command | What it does |
|
|
75
76
|
|---------|--------------|
|
|
76
|
-
| `loki "<task>"` |
|
|
77
|
-
| `loki
|
|
78
|
-
| `loki quick "<task>"` | One small task, no PR |
|
|
77
|
+
| `loki start "<task>"` | The primary path: build a task, PRD file or issue, verify it, seal a receipt, open a PR (`--no-pr`, `--deep`, `--provider`, `--max-cost`). `loki "<task>"` and `loki owner/repo#N` are shorthand |
|
|
78
|
+
| `loki edit "<change>"` | One targeted change in one pass, no PR, plan or council (`--verify`, `--receipt`, `--commit`, `--dry-run`) |
|
|
79
|
+
| `loki quick "<task>"` | One small task with a verify loop, no PR |
|
|
80
|
+
| `loki chat` | Interactive alternative: Plan and Build modes, slash commands, `/run` starts a `loki start` run, `--attach` joins one |
|
|
81
|
+
| `loki route start <args>` | Show the engine, provider, model and reason without running (also `--explain-route`) |
|
|
82
|
+
| `loki setup` | First-run wizard: tools, Claude login, provider, model route, spend cap |
|
|
83
|
+
| `loki config list\|get\|set\|path` | Read and write settings |
|
|
84
|
+
| `loki models [--local]` | List model routes (OpenRouter, Ollama, LM Studio, llama.cpp, vLLM, custom); discover local models |
|
|
85
|
+
| `loki agents list\|new\|show\|export --claude` | Custom child agents in `.loki/agents/*.md` |
|
|
79
86
|
| `loki status [run-id]` | Status of the latest run, or of a given run |
|
|
80
87
|
| `loki verify [run-id]` | Check a receipt's hash, signature and event log |
|
|
81
88
|
| `loki answer [run-id]` | Resume a BLOCKED run with your answer (`--text "..."`) |
|
|
82
|
-
| `loki doctor [--json]` | Check prerequisites, providers and login |
|
|
89
|
+
| `loki doctor [--fix] [--json]` | Check prerequisites, providers and login; `--fix` applies safe fixes |
|
|
83
90
|
| `loki control serve` | Start the Control Plane (see below) |
|
|
84
91
|
| `loki dashboard` | Open the Control Plane UI, starting it if needed |
|
|
85
92
|
| `loki tour` | Show a sample receipt offline |
|
|
86
93
|
| `loki --help` | Everything else; `loki help <command>` for one command |
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
Core verbs: [docs/CLI-CORE-VERBS.md](docs/CLI-CORE-VERBS.md). Every command (generated): [docs/CLI-REFERENCE.md](docs/CLI-REFERENCE.md). Engine guide, provider table, model routes, custom agents and chat: [docs/v10/GUIDE.md](docs/v10/GUIDE.md). Honest comparison with opencode: [docs/COMPARE-OPENCODE.md](docs/COMPARE-OPENCODE.md).
|
|
96
|
+
|
|
97
|
+
### Models, providers and local models
|
|
98
|
+
|
|
99
|
+
Claude Code is the default Tier-1 provider. `--provider codex|cline|aider|opencode` (or `LOKI_PROVIDER`) picks another; on all of them the Loki 10 engine enforces the wall-clock limit and a post-iteration guard scan, and measures cost for codex, cline and aider (parsed from CLI output or estimated, flagged `estimated`) so `--max-cost` applies. For OpenRouter or a local model, set `LOKI_MODEL_ROUTE=<kind>/<id>` (kinds: `openrouter`, `ollama`, `lmstudio`, `llamacpp`, `vllm`, `custom`) before `loki start`; `loki models --local` finds local runtimes and prints ready-to-paste values.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
LOKI_MODEL_ROUTE=ollama/qwen3-coder loki start prd.md
|
|
103
|
+
```
|
|
89
104
|
|
|
90
105
|
## Control Plane
|
|
91
106
|
|
|
@@ -105,15 +120,16 @@ While it is running, runs on this machine ship to it automatically. Guides: [doc
|
|
|
105
120
|
|
|
106
121
|
Required:
|
|
107
122
|
|
|
108
|
-
-
|
|
123
|
+
- Claude Code (`claude`), installed and logged in: the default provider and the only one with every feature. The bundled Agent SDK with `ANTHROPIC_API_KEY` can stand in on the main `loki start` path, but `loki edit` and `loki chat` call the `claude` CLI.
|
|
109
124
|
- Python 3.8+ (`python3`)
|
|
110
|
-
- Node.js
|
|
125
|
+
- Node.js 18+ (`node`; 20+ recommended)
|
|
111
126
|
- `jq`
|
|
112
127
|
- Git 2.x (`git`)
|
|
113
128
|
- `curl`
|
|
114
129
|
|
|
115
|
-
|
|
130
|
+
Optional:
|
|
116
131
|
|
|
132
|
+
- Other coding CLIs: Codex, Cline, Aider, opencode (`--provider`). Ollama or an OpenRouter key for model routes.
|
|
117
133
|
- Bun 1.3+ (`bun`) on your PATH (the npm package also bundles it).
|
|
118
134
|
- Docker, to run Loki from the published image.
|
|
119
135
|
|
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v11.
|
|
6
|
+
# Loki Mode v11.4.2
|
|
7
7
|
|
|
8
8
|
**Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
|
|
9
9
|
|
|
@@ -200,7 +200,7 @@ This protocol governs **skill module** loading -- task-scoped instruction files
|
|
|
200
200
|
<!-- generated:commands -->
|
|
201
201
|
Commands (generated from `loki-ts/src/cli/registry.ts`; full reference in `docs/CLI-REFERENCE.md`):
|
|
202
202
|
|
|
203
|
-
`version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `proof`, `wiki`, `control`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`
|
|
203
|
+
`version`, `status`, `doctor`, `provider`, `memory`, `rollback`, `proof`, `wiki`, `control`, `kpis`, `report`, `trust`, `crash`, `contract`, `start`, `slack`, `queue`, `issues`, `undo`, `verify-pr`, `answer`, `engine10`, `help`, `chat`, `edit`, `diff`, `models`, `setup`, `quick`, `quickstart`, `init`, `template`, `verify`, `keys`, `review`, `dashboard`, `config`, `mcp`, `acp`, `stop`, `pause`, `resume`, `why`, `next`, `logs`, `ship`, `deploy`, `import`, `github`, `issue`, `ci`, `modernize`, `share`, `assets`, `export`, `notify`, `tour`, `welcome`, `onboard`, `setup-skill`, `self-update`, `remote`, `cockpit`, `code`, `context`, `secrets`, `api`, `sandbox`, `docker`, `web`, `preview`, `telemetry`, `syslog`, `explain`, `docs`, `test`, `bench`, `voice`, `own`, `secure`, `compliance`, `enterprise`, `projects`, `audit`, `cost`, `metrics`, `sentrux`, `magic`
|
|
204
204
|
<!-- /generated -->
|
|
205
205
|
|
|
206
206
|
---
|
|
@@ -483,4 +483,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
483
483
|
|
|
484
484
|
---
|
|
485
485
|
|
|
486
|
-
**v11.
|
|
486
|
+
**v11.4.2 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
11.
|
|
1
|
+
11.4.2
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""User-configurable child agents: .loki/agents/<name>.md and ~/.config/loki/agents/<name>.md.
|
|
3
|
+
|
|
4
|
+
Markdown with simple YAML frontmatter (name, description, model, provider, tools,
|
|
5
|
+
deny, permission). Body is the system prompt. Project definitions override user ones.
|
|
6
|
+
Stdlib only. CLI: list | new | show | export | resolve.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
PERMISSIONS = ("plan", "edit", "full")
|
|
14
|
+
KEYS = ("name", "description", "model", "provider", "tools", "deny", "permission", "role")
|
|
15
|
+
LIST_KEYS = ("tools", "deny")
|
|
16
|
+
NAME_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
|
|
17
|
+
PROVIDERS = ("claude", "codex", "cline", "aider", "opencode")
|
|
18
|
+
# Tools a plan-permission agent must not use when exported to Claude Code.
|
|
19
|
+
PLAN_DENY = ["Edit", "Write", "NotebookEdit", "Bash"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AgentError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def project_dir():
|
|
27
|
+
return os.environ.get("LOKI_AGENTS_PROJECT_DIR") or os.path.join(os.getcwd(), ".loki", "agents")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def user_dir():
|
|
31
|
+
d = os.environ.get("LOKI_AGENTS_USER_DIR")
|
|
32
|
+
if d:
|
|
33
|
+
return d
|
|
34
|
+
base = os.environ.get("XDG_CONFIG_HOME") or os.path.join(os.path.expanduser("~"), ".config")
|
|
35
|
+
return os.path.join(base, "loki", "agents")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _unquote(v):
|
|
39
|
+
v = v.strip()
|
|
40
|
+
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
|
|
41
|
+
return v[1:-1]
|
|
42
|
+
return v
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _parse_list(v):
|
|
46
|
+
v = v.strip()
|
|
47
|
+
if v.startswith("[") and v.endswith("]"):
|
|
48
|
+
v = v[1:-1]
|
|
49
|
+
return [_unquote(x) for x in v.split(",") if x.strip()]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse(text, path="<agent>"):
|
|
53
|
+
lines = text.splitlines()
|
|
54
|
+
if not lines or lines[0].strip() != "---":
|
|
55
|
+
raise AgentError("%s: missing YAML frontmatter (file must start with '---')" % path)
|
|
56
|
+
end = None
|
|
57
|
+
for i in range(1, len(lines)):
|
|
58
|
+
if lines[i].strip() == "---":
|
|
59
|
+
end = i
|
|
60
|
+
break
|
|
61
|
+
if end is None:
|
|
62
|
+
raise AgentError("%s: frontmatter is not closed with '---'" % path)
|
|
63
|
+
meta = {}
|
|
64
|
+
cur = None
|
|
65
|
+
for n, raw in enumerate(lines[1:end], start=2):
|
|
66
|
+
if not raw.strip() or raw.lstrip().startswith("#"):
|
|
67
|
+
continue
|
|
68
|
+
m = re.match(r"^\s+-\s+(.*)$", raw)
|
|
69
|
+
if m and cur in LIST_KEYS:
|
|
70
|
+
meta.setdefault(cur, []).append(_unquote(m.group(1)))
|
|
71
|
+
continue
|
|
72
|
+
m = re.match(r"^([A-Za-z_]+)\s*:\s*(.*)$", raw)
|
|
73
|
+
if not m:
|
|
74
|
+
raise AgentError("%s:%d: cannot parse frontmatter line: %r" % (path, n, raw))
|
|
75
|
+
k, v = m.group(1).lower(), m.group(2)
|
|
76
|
+
if k not in KEYS:
|
|
77
|
+
raise AgentError("%s:%d: unknown key %r (allowed: %s)" % (path, n, k, ", ".join(KEYS)))
|
|
78
|
+
cur = k
|
|
79
|
+
if k in LIST_KEYS:
|
|
80
|
+
meta[k] = _parse_list(v) if v.strip() else []
|
|
81
|
+
else:
|
|
82
|
+
meta[k] = _unquote(v)
|
|
83
|
+
body = "\n".join(lines[end + 1:]).strip()
|
|
84
|
+
return meta, body
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate(meta, body, path, stem):
|
|
88
|
+
name = meta.get("name") or stem
|
|
89
|
+
if not NAME_RE.match(name):
|
|
90
|
+
raise AgentError("%s: invalid name %r (use lowercase letters, digits, '-' or '_')" % (path, name))
|
|
91
|
+
if name != stem:
|
|
92
|
+
raise AgentError("%s: name %r does not match file name %r" % (path, name, stem))
|
|
93
|
+
if not meta.get("description"):
|
|
94
|
+
raise AgentError("%s: 'description' is required" % path)
|
|
95
|
+
perm = meta.get("permission", "plan")
|
|
96
|
+
if perm not in PERMISSIONS:
|
|
97
|
+
raise AgentError("%s: permission must be one of %s (got %r)" % (path, "|".join(PERMISSIONS), perm))
|
|
98
|
+
prov = meta.get("provider", "claude")
|
|
99
|
+
if prov not in PROVIDERS:
|
|
100
|
+
raise AgentError("%s: provider must be one of %s (got %r)" % (path, "|".join(PROVIDERS), prov))
|
|
101
|
+
if not body:
|
|
102
|
+
raise AgentError("%s: body (system prompt) is empty" % path)
|
|
103
|
+
both = set(meta.get("tools", [])) & set(meta.get("deny", []))
|
|
104
|
+
if both:
|
|
105
|
+
raise AgentError("%s: tool(s) in both tools and deny: %s" % (path, ", ".join(sorted(both))))
|
|
106
|
+
return {
|
|
107
|
+
"name": name,
|
|
108
|
+
"description": meta["description"],
|
|
109
|
+
"model": meta.get("model", ""),
|
|
110
|
+
"provider": prov,
|
|
111
|
+
"tools": meta.get("tools", []),
|
|
112
|
+
"deny": meta.get("deny", []),
|
|
113
|
+
"permission": perm,
|
|
114
|
+
"role": meta.get("role", ""),
|
|
115
|
+
"prompt": body,
|
|
116
|
+
"path": path,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_file(path):
|
|
121
|
+
stem = os.path.basename(path)[:-3]
|
|
122
|
+
with open(path, encoding="utf-8") as f:
|
|
123
|
+
meta, body = parse(f.read(), path)
|
|
124
|
+
return validate(meta, body, path, stem)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_all():
|
|
128
|
+
"""Return (agents dict name->agent, errors list). Project overrides user."""
|
|
129
|
+
agents, errors = {}, []
|
|
130
|
+
for scope, d in (("user", user_dir()), ("project", project_dir())):
|
|
131
|
+
if not os.path.isdir(d):
|
|
132
|
+
continue
|
|
133
|
+
for fn in sorted(os.listdir(d)):
|
|
134
|
+
if not fn.endswith(".md") or fn.startswith("."):
|
|
135
|
+
continue
|
|
136
|
+
p = os.path.join(d, fn)
|
|
137
|
+
try:
|
|
138
|
+
a = load_file(p)
|
|
139
|
+
a["scope"] = scope
|
|
140
|
+
agents[a["name"]] = a
|
|
141
|
+
except (AgentError, OSError, UnicodeDecodeError) as e:
|
|
142
|
+
errors.append(str(e))
|
|
143
|
+
return agents, errors
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
TEMPLATE = """---
|
|
147
|
+
name: {name}
|
|
148
|
+
description: One line describing when to use this agent
|
|
149
|
+
model: sonnet
|
|
150
|
+
provider: claude
|
|
151
|
+
permission: plan
|
|
152
|
+
tools: [Read, Grep, Glob]
|
|
153
|
+
deny: []
|
|
154
|
+
---
|
|
155
|
+
You are the {name} agent. Describe your role, method and output format here.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_new(name, user=False):
|
|
160
|
+
if not NAME_RE.match(name or ""):
|
|
161
|
+
raise AgentError("invalid agent name %r (lowercase letters, digits, '-' or '_')" % name)
|
|
162
|
+
d = user_dir() if user else project_dir()
|
|
163
|
+
os.makedirs(d, exist_ok=True)
|
|
164
|
+
p = os.path.join(d, name + ".md")
|
|
165
|
+
if os.path.exists(p):
|
|
166
|
+
raise AgentError("already exists: %s" % p)
|
|
167
|
+
shipped = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "templates", "agents", name + ".md")
|
|
168
|
+
if os.path.isfile(shipped):
|
|
169
|
+
with open(shipped, encoding="utf-8") as src:
|
|
170
|
+
content = src.read()
|
|
171
|
+
else:
|
|
172
|
+
content = TEMPLATE.format(name=name)
|
|
173
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
174
|
+
f.write(content)
|
|
175
|
+
print("Created %s" % p)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def claude_export(a):
|
|
179
|
+
lines = ["---", "name: %s" % a["name"], "description: %s" % a["description"]]
|
|
180
|
+
if a["model"] and a["provider"] == "claude":
|
|
181
|
+
lines.append("model: %s" % a["model"])
|
|
182
|
+
tools = list(a["tools"])
|
|
183
|
+
if tools:
|
|
184
|
+
lines.append("tools: %s" % ", ".join(tools))
|
|
185
|
+
deny = list(a["deny"])
|
|
186
|
+
if a["permission"] == "plan":
|
|
187
|
+
deny += [t for t in PLAN_DENY if t not in deny and t not in tools]
|
|
188
|
+
if deny:
|
|
189
|
+
lines.append("disallowedTools: %s" % ", ".join(deny))
|
|
190
|
+
lines += ["---", a["prompt"], ""]
|
|
191
|
+
return "\n".join(lines)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
ALIASES = {
|
|
195
|
+
"code_quality_reviewer": ["reviewer"],
|
|
196
|
+
"requirements_verifier": ["reviewer"],
|
|
197
|
+
"test_auditor": ["tester", "reviewer"],
|
|
198
|
+
"devils_advocate": ["security", "reviewer"],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def resolve(role):
|
|
203
|
+
"""Pick a configured agent for a council role: exact name, then aliases."""
|
|
204
|
+
agents, _ = load_all()
|
|
205
|
+
for cand in [role] + ALIASES.get(role, []):
|
|
206
|
+
for k in (cand, cand.replace("_", "-")):
|
|
207
|
+
if k in agents:
|
|
208
|
+
return agents[k]
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def main(argv):
|
|
213
|
+
cmd = argv[0] if argv else "list"
|
|
214
|
+
rest = argv[1:]
|
|
215
|
+
try:
|
|
216
|
+
if cmd == "list":
|
|
217
|
+
agents, errors = load_all()
|
|
218
|
+
if not agents:
|
|
219
|
+
print("No configured agents. Create one with: loki agents new <name>")
|
|
220
|
+
else:
|
|
221
|
+
print("%-16s %-8s %-8s %-6s %s" % ("NAME", "SCOPE", "PROVIDER", "PERM", "DESCRIPTION"))
|
|
222
|
+
for n in sorted(agents):
|
|
223
|
+
a = agents[n]
|
|
224
|
+
print("%-16s %-8s %-8s %-6s %s" % (n, a["scope"], a["provider"], a["permission"], a["description"]))
|
|
225
|
+
for e in errors:
|
|
226
|
+
sys.stderr.write("invalid agent: %s\n" % e)
|
|
227
|
+
return 1 if errors else 0
|
|
228
|
+
if cmd == "new":
|
|
229
|
+
names = [x for x in rest if not x.startswith("--")]
|
|
230
|
+
if not names:
|
|
231
|
+
raise AgentError("usage: loki agents new <name> [--user]")
|
|
232
|
+
cmd_new(names[0], "--user" in rest)
|
|
233
|
+
return 0
|
|
234
|
+
if cmd == "show":
|
|
235
|
+
if not rest:
|
|
236
|
+
raise AgentError("usage: loki agents show <name>")
|
|
237
|
+
agents, errors = load_all()
|
|
238
|
+
a = agents.get(rest[0])
|
|
239
|
+
if not a:
|
|
240
|
+
for e in errors:
|
|
241
|
+
sys.stderr.write("invalid agent: %s\n" % e)
|
|
242
|
+
raise AgentError("agent not found: %s (see: loki agents list)" % rest[0])
|
|
243
|
+
for k in ("name", "scope", "path", "description", "provider", "model", "permission", "tools", "deny"):
|
|
244
|
+
v = a[k]
|
|
245
|
+
print("%-12s %s" % (k + ":", ", ".join(v) if isinstance(v, list) else (v or "(default)")))
|
|
246
|
+
print("\n" + a["prompt"])
|
|
247
|
+
return 0
|
|
248
|
+
if cmd == "export":
|
|
249
|
+
if "--claude" not in rest:
|
|
250
|
+
raise AgentError("usage: loki agents export --claude [--user]")
|
|
251
|
+
agents, errors = load_all()
|
|
252
|
+
for e in errors:
|
|
253
|
+
sys.stderr.write("invalid agent: %s\n" % e)
|
|
254
|
+
if not agents:
|
|
255
|
+
raise AgentError("no configured agents to export")
|
|
256
|
+
if "--user" in rest:
|
|
257
|
+
out = os.path.expanduser("~/.claude/agents")
|
|
258
|
+
else:
|
|
259
|
+
out = os.path.join(os.getcwd(), ".claude", "agents")
|
|
260
|
+
os.makedirs(out, exist_ok=True)
|
|
261
|
+
for n in sorted(agents):
|
|
262
|
+
p = os.path.join(out, n + ".md")
|
|
263
|
+
with open(p, "w", encoding="utf-8") as f:
|
|
264
|
+
f.write(claude_export(agents[n]))
|
|
265
|
+
print("Wrote %s" % p)
|
|
266
|
+
return 1 if errors else 0
|
|
267
|
+
if cmd == "resolve":
|
|
268
|
+
a = resolve(rest[0]) if rest else None
|
|
269
|
+
if a:
|
|
270
|
+
print(json.dumps({"name": a["name"], "model": a["model"], "provider": a["provider"],
|
|
271
|
+
"permission": a["permission"], "deny": a["deny"], "prompt": a["prompt"]}))
|
|
272
|
+
return 0
|
|
273
|
+
raise AgentError("unknown subcommand %r (list|new|show|export)" % cmd)
|
|
274
|
+
except AgentError as e:
|
|
275
|
+
sys.stderr.write("Error: %s\n" % e)
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
if __name__ == "__main__":
|
|
280
|
+
sys.exit(main(sys.argv[1:]))
|
package/autonomy/council-v2.sh
CHANGED
|
@@ -339,6 +339,27 @@ council_v2_run_reviewer() {
|
|
|
339
339
|
;;
|
|
340
340
|
esac
|
|
341
341
|
|
|
342
|
+
# User-configured agent (loki agents) for this role overrides prompt/model.
|
|
343
|
+
# No configured agents (the default) leaves everything unchanged.
|
|
344
|
+
local _c2_agent_model=""
|
|
345
|
+
local _c2_agents_helper
|
|
346
|
+
_c2_agents_helper="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/agents/custom_agents.py"
|
|
347
|
+
if [ -f "$_c2_agents_helper" ] && [ -n "$_c2_py" ] \
|
|
348
|
+
&& { [ -d "${PWD}/.loki/agents" ] || [ -d "${XDG_CONFIG_HOME:-$HOME/.config}/loki/agents" ] || [ -n "${LOKI_AGENTS_USER_DIR:-}" ]; }; then
|
|
349
|
+
local _c2_agent_json
|
|
350
|
+
_c2_agent_json="$("$_c2_py" -I "$_c2_agents_helper" resolve "$role" 2>/dev/null)" || _c2_agent_json=""
|
|
351
|
+
if [ -n "$_c2_agent_json" ]; then
|
|
352
|
+
local _c2_agent_prompt
|
|
353
|
+
_c2_agent_prompt="$(printf '%s' "$_c2_agent_json" | "$_c2_py" -I -c 'import sys,json; print(json.load(sys.stdin)["prompt"])' 2>/dev/null)" || _c2_agent_prompt=""
|
|
354
|
+
_c2_agent_model="$(printf '%s' "$_c2_agent_json" | "$_c2_py" -I -c 'import sys,json; d=json.load(sys.stdin); print(d["model"] if d["provider"]=="claude" else "")' 2>/dev/null)" || _c2_agent_model=""
|
|
355
|
+
if [ -n "$_c2_agent_prompt" ]; then
|
|
356
|
+
prompt="$_c2_agent_prompt
|
|
357
|
+
|
|
358
|
+
Output a JSON object with keys: verdict (APPROVE or REJECT), reasoning (string), issues (array of {severity, description})."
|
|
359
|
+
fi
|
|
360
|
+
fi
|
|
361
|
+
fi
|
|
362
|
+
|
|
342
363
|
local evidence=""
|
|
343
364
|
[ -f "$review_dir/evidence.md" ] && evidence=$(cat "$review_dir/evidence.md")
|
|
344
365
|
local prd=""
|
|
@@ -404,7 +425,7 @@ Respond ONLY with a valid JSON object. No markdown fencing."
|
|
|
404
425
|
# both apply (a reviewer must never mutate the tree). Gated +
|
|
405
426
|
# opt-out LOKI_BARE_SUBCALLS=0 / LOKI_REVIEW_TOOL_GUARD=0;
|
|
406
427
|
# type-guarded for standalone sourcing.
|
|
407
|
-
local _c2_argv=("--model" "haiku")
|
|
428
|
+
local _c2_argv=("--model" "${_c2_agent_model:-haiku}")
|
|
408
429
|
if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
|
|
409
430
|
_c2_argv+=("--bare")
|
|
410
431
|
fi
|