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.
Files changed (103) hide show
  1. package/README.md +34 -18
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/agents/custom_agents.py +280 -0
  5. package/autonomy/council-v2.sh +22 -1
  6. package/autonomy/edit.sh +242 -0
  7. package/autonomy/lib/chat.sh +312 -0
  8. package/autonomy/lib/help-core.sh +164 -0
  9. package/autonomy/lib/json-output.sh +38 -0
  10. package/autonomy/lib/run-summary.sh +96 -0
  11. package/autonomy/loki +105 -10
  12. package/autonomy/route.sh +270 -0
  13. package/autonomy/run.sh +6 -0
  14. package/autonomy/setup-wizard.sh +161 -0
  15. package/autonomy/verify.sh +22 -2
  16. package/bin/loki +73 -18
  17. package/completions/_loki +55 -0
  18. package/completions/loki.bash +49 -3
  19. package/dashboard/__init__.py +1 -1
  20. package/docs/CLI-CORE-VERBS.md +112 -0
  21. package/docs/CLI-REFERENCE.md +7 -1
  22. package/docs/COMPARE-OPENCODE.md +94 -0
  23. package/docs/COMPONENTS.md +1 -0
  24. package/docs/INSTALLATION.md +56 -12
  25. package/docs/KNOWN-ISSUES.md +2 -12
  26. package/docs/QUICKSTART.md +108 -0
  27. package/docs/environment-variables.md +9 -2
  28. package/docs/exit-codes.md +29 -12
  29. package/docs/v10/CLI-MODERN.md +10 -4
  30. package/docs/v10/FAILURE-CLASSES.md +2 -2
  31. package/docs/v10/GUIDE.md +270 -44
  32. package/docs/v10/METRICS.md +6 -0
  33. package/docs/v10/OPENCODE-PARITY-PLAN.md +84 -0
  34. package/docs/v10/PROGRESS.md +7 -0
  35. package/loki-ts/dist/loki.js +968 -933
  36. package/mcp/__init__.py +1 -1
  37. package/package.json +2 -2
  38. package/packages/control-plane/dist/server.js +198 -86
  39. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  40. package/providers/claude.sh +16 -2
  41. package/providers/model_route_exec.sh +22 -0
  42. package/providers/model_routes.sh +188 -0
  43. package/skills/providers.md +13 -0
  44. package/templates/agents/planner.md +10 -0
  45. package/templates/agents/reviewer.md +10 -0
  46. package/templates/agents/tester.md +10 -0
  47. package/web-app/dist/assets/{AdminPage-D1jlQGeE.js → AdminPage-CYJ2BWRC.js} +1 -1
  48. package/web-app/dist/assets/{Avatar-QUX_aJfQ.js → Avatar-DdK4wynI.js} +1 -1
  49. package/web-app/dist/assets/{Badge-pysXUR6J.js → Badge-CrX3MI7J.js} +1 -1
  50. package/web-app/dist/assets/{Button-CWzlTw9P.js → Button-709-wwGM.js} +1 -1
  51. package/web-app/dist/assets/{CockpitPage-DLCoLdyF.js → CockpitPage-BapdTInO.js} +1 -1
  52. package/web-app/dist/assets/{ComparePage-CCENGIti.js → ComparePage-DT99pGvA.js} +1 -1
  53. package/web-app/dist/assets/{ErrorBoundary-BssCcbgv.js → ErrorBoundary-B0uJSys8.js} +1 -1
  54. package/web-app/dist/assets/{EvidenceReceiptPanel-CwmAqJ90.js → EvidenceReceiptPanel-BtMFgElP.js} +1 -1
  55. package/web-app/dist/assets/{GitHubIssuesPanel-DuSw8shb.js → GitHubIssuesPanel-DjJuU941.js} +1 -1
  56. package/web-app/dist/assets/{GitHubPRsPanel-CGvLbDjh.js → GitHubPRsPanel-HXfEX1BC.js} +1 -1
  57. package/web-app/dist/assets/{HomePage-BrBX5Gn2.js → HomePage-Cqyves-S.js} +2 -2
  58. package/web-app/dist/assets/{LoginPage-DPjr7QHU.js → LoginPage-jUbtC7_Y.js} +1 -1
  59. package/web-app/dist/assets/{MagicPage-Dxd7FWF2.js → MagicPage-DHRsqjgR.js} +1 -1
  60. package/web-app/dist/assets/{MetricsPage-BP47Urni.js → MetricsPage-B-pED05R.js} +1 -1
  61. package/web-app/dist/assets/{NotFoundPage-D7_07YoB.js → NotFoundPage-CsbUs21R.js} +1 -1
  62. package/web-app/dist/assets/{ProjectPage-SQ183efy.js → ProjectPage-BtlLEQwB.js} +3 -3
  63. package/web-app/dist/assets/{ProjectsPage-Cgr1K2Ae.js → ProjectsPage-DaRbRjSp.js} +1 -1
  64. package/web-app/dist/assets/{SettingsPage-kyYOSDt0.js → SettingsPage-DALrtP3F.js} +1 -1
  65. package/web-app/dist/assets/{ShowcasePage-CRd-H1A4.js → ShowcasePage-CoPHSNP0.js} +1 -1
  66. package/web-app/dist/assets/{SystemSettingsPage-BHBdGXod.js → SystemSettingsPage-kOuSN50O.js} +1 -1
  67. package/web-app/dist/assets/{TeamsPage-CI3Wdbjw.js → TeamsPage-DGWK0LMp.js} +1 -1
  68. package/web-app/dist/assets/{TemplatesPage-DfhCgP6i.js → TemplatesPage-C-_10CwA.js} +1 -1
  69. package/web-app/dist/assets/{TerminalOutput-Cs87YXLJ.js → TerminalOutput-Cz_vPbXL.js} +1 -1
  70. package/web-app/dist/assets/{activity-CrQx5SbK.js → activity-icIsXJVa.js} +1 -1
  71. package/web-app/dist/assets/{bell-Dde5IbnI.js → bell-DbPR2_4r.js} +1 -1
  72. package/web-app/dist/assets/{bot-trRuIJT6.js → bot-DvjP3Yh5.js} +1 -1
  73. package/web-app/dist/assets/{check-DmsthR-3.js → check-D1OBxHeU.js} +1 -1
  74. package/web-app/dist/assets/{chevron-left-C5wZb7mg.js → chevron-left-D09jDZ29.js} +1 -1
  75. package/web-app/dist/assets/{circle-alert-BCb173fK.js → circle-alert-DyZMLOcC.js} +1 -1
  76. package/web-app/dist/assets/{clock-CvcqL3Ma.js → clock-CdpdJTId.js} +1 -1
  77. package/web-app/dist/assets/{cloud-CW1r3-AN.js → cloud-CoLTlmoj.js} +1 -1
  78. package/web-app/dist/assets/{code-xml-uxBTXAfZ.js → code-xml-DrkE2ZZY.js} +1 -1
  79. package/web-app/dist/assets/{database-DBrvW0o1.js → database-CMQQuM2R.js} +1 -1
  80. package/web-app/dist/assets/{dollar-sign-WrOyoDcN.js → dollar-sign-CuioDaOG.js} +1 -1
  81. package/web-app/dist/assets/{file-code-corner-8JC8TI2_.js → file-code-corner-Cx9j9K1S.js} +1 -1
  82. package/web-app/dist/assets/{file-plus-CgUdOr2u.js → file-plus-CYD2zaaI.js} +1 -1
  83. package/web-app/dist/assets/{globe-w-0x46nt.js → globe-6w-hnQkL.js} +1 -1
  84. package/web-app/dist/assets/{hammer-Civ6_wQq.js → hammer-BpqtautL.js} +1 -1
  85. package/web-app/dist/assets/{index-wvGJotyF.js → index-Dzs-9EgJ.js} +3 -3
  86. package/web-app/dist/assets/{layers-Cc3wLG39.js → layers-D5hd7jb7.js} +1 -1
  87. package/web-app/dist/assets/{loader-circle-BedHqfS4.js → loader-circle-C6Y9x_7H.js} +1 -1
  88. package/web-app/dist/assets/{lock-CRyzk3QG.js → lock-D51L7rYh.js} +1 -1
  89. package/web-app/dist/assets/{package-DCT1v7W2.js → package-BJF8hkP8.js} +1 -1
  90. package/web-app/dist/assets/{plus-C7eerOb_.js → plus-D5kUQ79n.js} +1 -1
  91. package/web-app/dist/assets/{refresh-cw-DO753Y_B.js → refresh-cw-BKdlCa7F.js} +1 -1
  92. package/web-app/dist/assets/{rotate-ccw-ptlyGV4I.js → rotate-ccw-_bd_YI1e.js} +1 -1
  93. package/web-app/dist/assets/{scroll-text-CqPwk4yI.js → scroll-text-DbkegIxF.js} +1 -1
  94. package/web-app/dist/assets/{server-BP_VvY9m.js → server-DAwqFIsq.js} +1 -1
  95. package/web-app/dist/assets/{shield-alert-Bf0waB0a.js → shield-alert-BuR1tPHu.js} +1 -1
  96. package/web-app/dist/assets/{thumbs-up-p1roSIsw.js → thumbs-up-BF6p3mRd.js} +1 -1
  97. package/web-app/dist/assets/{trash-2-BrMaXAxl.js → trash-2-BVpnkqqz.js} +1 -1
  98. package/web-app/dist/assets/{trending-down-DaGcCn2W.js → trending-down-DpsDvRwW.js} +1 -1
  99. package/web-app/dist/assets/{trending-up-CKPupOSE.js → trending-up-mFogTjt3.js} +1 -1
  100. package/web-app/dist/assets/{upload-CR0gzibW.js → upload-4T8zpBsi.js} +1 -1
  101. package/web-app/dist/assets/{usePolling-kKzJRjFw.js → usePolling-DPXQv8WO.js} +1 -1
  102. package/web-app/dist/assets/{user-CPOXPTtD.js → user-BYGIFaTm.js} +1 -1
  103. 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
  [![Docker Pulls](https://img.shields.io/docker/pulls/asklokesh/loki-mode?style=for-the-badge&logo=docker&logoColor=white&color=2F71E3)](https://hub.docker.com/r/asklokesh/loki-mode)
8
8
  [![License](https://img.shields.io/badge/License-BUSL--1.1-36342E?style=for-the-badge)](LICENSE)
9
9
 
10
- [Website](https://www.autonomi.dev/) | [Install guide](docs/INSTALLATION.md) | [User guide](docs/v10/GUIDE.md) | [CLI reference](docs/CLI-REFERENCE.md) | [Changelog](CHANGELOG.md)
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. An `ANTHROPIC_API_KEY` is enough, or an installed and logged-in [Claude Code](https://docs.claude.com/en/docs/claude-code) CLI.
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 doctor # names anything missing, exit 0 when ready
40
- loki quick "fix the login redirect loop" # one small task, no PR
41
- loki "add rate limiting to /api/login" # build, verify, seal a receipt, open a PR
42
- loki owner/repo#123 # same, from a GitHub, GitLab or Jira issue
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 (default $100 with an API key, no dollar cap on a subscription), and `--provider <name>` to pick another coding CLI.
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 VERIFIED (exit 0), TAMPERED (1), UNCHECKED (2) or UNSIGNED (3). 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
+ `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>"` | Build a task, verify it, seal a receipt, open a PR (`--no-pr`, `--deep`, `--provider`, `--max-cost`) |
77
- | `loki owner/repo#N` | The same, from a GitHub, GitLab or Jira issue (issue URLs work too) |
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
- Full reference: [docs/CLI-REFERENCE.md](docs/CLI-REFERENCE.md). Engine guide, provider table and model routing: [docs/v10/GUIDE.md](docs/v10/GUIDE.md).
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
- - A model: `ANTHROPIC_API_KEY`, or a provider CLI such as Claude Code (`claude`). Cline, Codex, Aider and opencode are supported as experimental providers.
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 20+ (`node`)
125
+ - Node.js 18+ (`node`; 20+ recommended)
111
126
  - `jq`
112
127
  - Git 2.x (`git`)
113
128
  - `curl`
114
129
 
115
- Recommended:
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.3.8
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.3.8 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
486
+ **v11.4.2 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.3.8
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:]))
@@ -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