loki-mode 10.6.7 → 10.6.9
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 +44 -9
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/backlog.py +167 -36
- package/autonomy/lib/workspace.py +384 -0
- package/autonomy/lib/workspace_comment.py +91 -0
- package/autonomy/lib/worktree_prep.py +143 -0
- package/autonomy/loki +96 -3
- package/bin/loki +3 -3
- package/completions/_loki +2 -0
- package/completions/loki.bash +1 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +26 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
- package/docs/WORKSPACES.md +36 -0
- package/docs/alternative-installations.md +3 -1
- package/docs/control-plane-container.md +50 -0
- package/docs/dashboard-guide.md +4 -0
- package/docs/dev/architecture-reference.md +4 -0
- package/docs/dev/project-structure.md +1 -1
- package/docs/exit-codes.md +1 -0
- package/docs/slack.md +34 -0
- package/docs/trackers.md +27 -0
- package/docs/v10/BOARD.md +74 -14
- package/docs/v10/CONTROL-PLANE.md +17 -6
- package/docs/v10/D51-PHASE-B.md +3 -1
- package/docs/v10/D61-SPEED.md +11 -2
- package/docs/v10/D63-CARDS.md +354 -0
- package/docs/v10/DECISIONS.md +63 -0
- package/docs/v10/DEPS.md +6 -2
- package/docs/v10/E2E-D65.md +87 -0
- package/docs/v10/HANDOFF-NEXT-WEEK.md +10 -4
- package/docs/v10/PROGRESS.md +63 -0
- package/docs/v10/ROADMAP-D63.md +35 -0
- package/loki-ts/dist/loki.js +612 -556
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +76 -0
- package/mcp/tests/test_v10_tools.py +193 -0
- package/mcp/v10_tools.py +182 -0
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +131 -9
- package/packages/control-plane/drizzle/0001_group_id.sql +3 -0
- package/packages/control-plane/drizzle/meta/_journal.json +8 -1
- package/packages/control-plane/src/db/schema.ts +3 -0
- package/packages/control-plane/src/server/answer.ts +33 -0
- package/packages/control-plane/src/server/app.ts +22 -2
- package/packages/control-plane/src/server/auth.ts +51 -0
- package/packages/control-plane/src/server/runs.ts +6 -3
- package/packages/control-plane/src/server/serve.ts +13 -3
- package/packages/control-plane/src/shipper/discover.ts +29 -0
- package/packages/control-plane/ui/dist/assets/{index-BH3LtKcG.css → index-CFfrqCx2.css} +1 -1
- package/packages/control-plane/ui/dist/assets/index-Jt31nrK-.js +114 -0
- package/packages/control-plane/ui/dist/index.html +2 -2
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-DyjnUcaP.js → AdminPage-CZKZLuWU.js} +1 -1
- package/web-app/dist/assets/{Avatar-CY0sQNao.js → Avatar-Dcy9kO2E.js} +1 -1
- package/web-app/dist/assets/{Badge-BmwTRu72.js → Badge-DUotUSyx.js} +1 -1
- package/web-app/dist/assets/{Button-BRf5aTlb.js → Button-CFz_zfMZ.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-RR94HuxS.js → CockpitPage-BhRw_oiM.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CFWAuPRO.js → ComparePage-DfnsRTTC.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-4cDADrG-.js → ErrorBoundary-Dn0gUu3D.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-ChPFx2eM.js → EvidenceReceiptPanel-Co-xi282.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-B0TAjyeW.js → GitHubIssuesPanel-CoCpfexq.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BbszEwW9.js → GitHubPRsPanel-Dg0Eg6F_.js} +1 -1
- package/web-app/dist/assets/{HomePage-DrBgzs1d.js → HomePage-BysxL4mV.js} +2 -2
- package/web-app/dist/assets/{LoginPage-Bu7b2DGz.js → LoginPage-CQ366p_d.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BFEYp631.js → MagicPage-DD7YcbNq.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-eIe2naVR.js → MetricsPage-BYz49xQj.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BVWYkNt9.js → NotFoundPage-CKs09MeO.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-zmxyu732.js → ProjectPage-Ci2BIA_D.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-Dq5NdCak.js → ProjectsPage-ClwMH0eu.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-CCrJOMe0.js → SettingsPage-Cm0rCX-y.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-n-Ipzp01.js → ShowcasePage-CxvESt8X.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-CP_rjYXK.js → SystemSettingsPage-W_CGYPOY.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B_P9DepW.js → TeamsPage-feVvRvys.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-h-3A6PQT.js → TemplatesPage-C-pUWMmF.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-pD0nJ1BU.js → TerminalOutput-j5ibjKY3.js} +1 -1
- package/web-app/dist/assets/{activity-ChFwzT-d.js → activity-TTdVYMa_.js} +1 -1
- package/web-app/dist/assets/{bell-BX-eK8Gu.js → bell-DeoTFwpv.js} +1 -1
- package/web-app/dist/assets/{bot-fj5PkPtR.js → bot-B5SghxqU.js} +1 -1
- package/web-app/dist/assets/{check-BGAbHlvD.js → check-k8SzsdbE.js} +1 -1
- package/web-app/dist/assets/{chevron-left-BzFUxHgb.js → chevron-left-DHnEllFz.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BQrg5DWl.js → circle-alert-DW_SLdcB.js} +1 -1
- package/web-app/dist/assets/{clock-CTu9lkVo.js → clock-Bmoaxy09.js} +1 -1
- package/web-app/dist/assets/{cloud-DrIENRiE.js → cloud-eg3tzXTh.js} +1 -1
- package/web-app/dist/assets/{code-xml-PaoVpfur.js → code-xml-VAfXihhD.js} +1 -1
- package/web-app/dist/assets/{database-DMmkAotn.js → database-CY3_-5aK.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-B2LQdBoX.js → dollar-sign-DYC0OLlg.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Dfv1XXeR.js → file-code-corner-0X3SveKz.js} +1 -1
- package/web-app/dist/assets/{file-plus-D2Wzf2wd.js → file-plus-B_oh_ukl.js} +1 -1
- package/web-app/dist/assets/{globe-DxotVWt8.js → globe-F0tLA8WJ.js} +1 -1
- package/web-app/dist/assets/{hammer-Brfy5ZrJ.js → hammer-DrlvBmfW.js} +1 -1
- package/web-app/dist/assets/{index-BfhHMwi8.js → index-YihVaZAj.js} +3 -3
- package/web-app/dist/assets/{layers-DfxSWDvp.js → layers-CbJfn5UC.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CxIrzNwx.js → loader-circle-CpmOggou.js} +1 -1
- package/web-app/dist/assets/{lock-m6nzhtpU.js → lock-BGJ6QWrI.js} +1 -1
- package/web-app/dist/assets/{package-ri9oE9Jc.js → package-BTmH4Xto.js} +1 -1
- package/web-app/dist/assets/{plus-DWeTAMVV.js → plus-mKQ7hz-8.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-BGMSpaMh.js → refresh-cw-vPetpLsm.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DD1xBD_b.js → rotate-ccw-DiJ8GFhj.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BCCR_AN9.js → scroll-text-DpPQIlQR.js} +1 -1
- package/web-app/dist/assets/{server-CmMWPIjS.js → server-CiaV4drx.js} +1 -1
- package/web-app/dist/assets/{shield-alert-BZZmBVEJ.js → shield-alert-BaaQqyco.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-OaxbNRXu.js → thumbs-up-2zNCKp0Q.js} +1 -1
- package/web-app/dist/assets/{trash-2-DmcSzTfr.js → trash-2-CiVDx5Co.js} +1 -1
- package/web-app/dist/assets/{trending-down-BmfXLykz.js → trending-down-tcxvw5BH.js} +1 -1
- package/web-app/dist/assets/{trending-up-Djec3jIq.js → trending-up-C2bv2_v_.js} +1 -1
- package/web-app/dist/assets/{upload-BN8dXIlF.js → upload-DS4VPUAp.js} +1 -1
- package/web-app/dist/assets/{usePolling-Cu4yi04I.js → usePolling-JggBcVln.js} +1 -1
- package/web-app/dist/assets/{user-IBI-k10o.js → user-CvavvOxM.js} +1 -1
- package/web-app/dist/index.html +1 -1
- package/packages/control-plane/ui/dist/assets/index-B6EiyukW.js +0 -114
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ An autonomous software factory that knows what it is supposed to deliver, and pr
|
|
|
11
11
|
|
|
12
12
|
Loki Mode is a free, source-available autonomous coding agent by [Autonomi](https://www.autonomi.dev/). Hand it a PRD, GitHub issue, OpenAPI doc or one-line brief. It derives a delivery contract (the acceptance criteria), builds against it, and ends with a signed Evidence Receipt that states what was proven and what was not. If the contract cannot be derived, it stops and asks one question instead of guessing.
|
|
13
13
|
|
|
14
|
-
Before a build counts as done, a review council selects reviewers from a specialist pool (`agents/types.json`, scored by `run.sh:FOCUS_KEYWORDS`). The bundled MCP server exposes
|
|
14
|
+
Before a build counts as done, a review council selects reviewers from a specialist pool (`agents/types.json`, scored by `run.sh:FOCUS_KEYWORDS`). The bundled MCP server exposes 39 tools over stdio (`mcp/server.py`), including `loki_v10_run`, `loki_v10_status` and `loki_v10_verify` to start a Loki 10 run in the background, read its phase, verdict and cost, and verify its receipt (`BLOCKED` runs report their question). Add it with `claude mcp add loki-mode -- python3 -m mcp.server` from the install directory.
|
|
15
15
|
|
|
16
16
|
## Contents
|
|
17
17
|
|
|
@@ -120,6 +120,8 @@ flowchart TD
|
|
|
120
120
|
| pr | Opened by the supervisor, not the worker (the worker never holds a GitHub token). A non-VERIFIED run opens a draft PR. The body is written for a 60-second review: what the issue asked, what changed, how it was tested, NOT PROVEN, then the receipt digest and `loki verify`. Data the run did not record prints "not recorded". |
|
|
121
121
|
| deep verify | Detached, started after the PR opens (so not with `--no-pr`): full suite, app-boot probe, council, secret scan. A check that is refused or unavailable is reported as NOT PROVEN, never as red. `--deep` is a separate flag that raises the implement and run budgets. |
|
|
122
122
|
|
|
123
|
+
Warm start (experimental, `LOKI_SPEED=1`, off by default): `loki engine10 dashboard` also serves a unix socket at `~/.loki/run/engine.sock` (override with `LOKI_WARM_SOCK`) that keeps the repo map and test map in memory, keyed by the tree SHA plus a hash of dirty files, so an edit invalidates them. A run tries the socket for 50ms and prints `warm in Xs` when it answers; with no daemon it runs the cold path with identical events.
|
|
124
|
+
|
|
123
125
|
PR body sample (`loki-ts/src/e10ext/reviewer_body.ts`):
|
|
124
126
|
|
|
125
127
|
```
|
|
@@ -151,7 +153,29 @@ The run cap is 900s (2700s with `--deep`, `DEFAULT_CAP_S` and `DEEP_CAP_S` in `t
|
|
|
151
153
|
|
|
152
154
|
A finished run prints a short summary (see [Quiet output](#quiet-output), which also shows an example) whose `NOT PROVEN` line is never empty by omission: deep checks deferred to the deep-verify pass are always listed there.
|
|
153
155
|
|
|
154
|
-
`loki status [run-id]` and `loki verify [run-id]` are built on the v10 path (bare `loki verify` follows the newest run, v10 or legacy). `loki dashboard` and `loki status` reach the v10 commands with `LOKI_ENGINE=v10`. Slack
|
|
156
|
+
`loki status [run-id]` and `loki verify [run-id]` are built on the v10 path (bare `loki verify` follows the newest run, v10 or legacy). `loki dashboard` and `loki status` reach the v10 commands with `LOKI_ENGINE=v10`. Two-way Slack (`loki slack serve`) is on by default and documented in [docs/slack.md](docs/slack.md); outbound notifications use `LOKI_SLACK_WEBHOOK_URL`.
|
|
157
|
+
|
|
158
|
+
## Run from Jira or Linear
|
|
159
|
+
|
|
160
|
+
Loki 10 can start from a Jira or Linear issue the same way it starts from a GitHub issue: same run, same receipt. The issue is fetched deterministically before any model runs and normalized into the same `issue.json`, with a `source` field of `jira` or `linear`.
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
loki jira:PROJ-123 # or https://<site>.atlassian.net/browse/PROJ-123
|
|
164
|
+
loki linear:ENG-42 # or a linear.app issue URL
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- Jira needs `JIRA_EMAIL` and `JIRA_API_TOKEN`, plus `JIRA_BASE_URL` (for example `https://acme.atlassian.net`) unless you pass the full browse URL. The ADF description is converted to plain text.
|
|
168
|
+
- Linear needs `LINEAR_API_KEY`.
|
|
169
|
+
- A missing variable stops the run before any work with an error naming it (exit code 2, like other intake errors). GitHub refs are unchanged.
|
|
170
|
+
- Self-hosted Jira: `<JIRA_BASE_URL>/browse/KEY` is accepted when its origin matches `JIRA_BASE_URL`. `LOKI_TRACKER_INTAKE=0` turns tracker intake off. Intake only, no sync. Details: [docs/trackers.md](docs/trackers.md).
|
|
171
|
+
|
|
172
|
+
## Two-way Slack
|
|
173
|
+
|
|
174
|
+
Two-way Slack is on by default. With `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` in the environment, `loki slack serve --port N` (127.0.0.1 by default) lets you mention `@loki <issue ref or task>` in a Slack thread to start a run, and a BLOCKED question is answered in the same thread. With either variable missing it prints one line naming both and exits 2 without binding a port. Set `LOKI_SLACK_INBOUND=0` to disable it. See [docs/slack.md](docs/slack.md) for the scopes.
|
|
175
|
+
|
|
176
|
+
## Spec to contract
|
|
177
|
+
|
|
178
|
+
On by default; set `LOKI_CONTRACT=0` to turn it off. It does nothing unless `.loki/contract.json` exists. `loki contract <spec.md>` parses a spec or PRD into numbered acceptance criteria (AC-1, AC-2, up to 50) from checklist items (`- [ ]`) and from bullets and numbered lists under headings such as "Acceptance criteria", "Requirements" or "Must", prints them, and writes `.loki/contract.json` at the repo root. When a run seals with a contract present, each criterion is matched to changed files and checks by keyword overlap and recorded in an optional `contract` field on the receipt (`keyword_match` or `no_match`). Unmatched criteria are listed under NOT PROVEN in the receipt and the PR body (advisory only, the verdict is unchanged). Keyword overlap is a heuristic: `keyword_match` means a changed file plausibly relates to the criterion, not that the criterion is proven. The legacy PRD path is unchanged.
|
|
155
179
|
|
|
156
180
|
## Triggers without a cloud
|
|
157
181
|
|
|
@@ -210,6 +234,10 @@ loki verify
|
|
|
210
234
|
|
|
211
235
|
A verified receipt is bound to the run's event log, so a receipt lifted out of its run, or a log edited after sealing, does not verify. Signing proves the receipt came from the key holder; it does not prove the generated code is bug-free. A receipt only claims what its checks ran, and states what they did not.
|
|
212
236
|
|
|
237
|
+
### Visual evidence (opt-in)
|
|
238
|
+
|
|
239
|
+
With `LOKI_VISUAL_EVIDENCE=1`, a v10 run that changed web page files (html, jsx, tsx, vue, svelte under app/, pages/, src/ or public/) starts the repo's dev, preview or start script and screenshots each changed route with the repo's own Playwright (nothing is downloaded) into a fresh per-run directory `.loki/runs/<run_id>/evidence/<random>/` (at most 5 routes, total capture budget 25s, aborted with the seal stage). API-only repos with an openapi file get an HTTP transcript at `.loki/runs/<run_id>/evidence/<random>/http.json` (not hashed into the receipt). Each screenshot's sha256 is recorded in the receipt as `evidence_screens`, the PR body gets an Evidence section, and `loki verify` reports TAMPERED if a recorded screenshot is altered, missing or a symlink. Capture never fails the run; a skip is listed in NOT PROVEN. Off by default.
|
|
240
|
+
|
|
213
241
|
## Quiet output
|
|
214
242
|
|
|
215
243
|
The v10 run is quiet by default: no stage chatter, only the final summary. `--verbose` tails the event stream live (one line per finished stage and a heartbeat with elapsed time and the running diff size). `--json` prints one JSON object (`ok`, `outcome`, `stop`, `run_id`, `receipt_sha256`) instead of text.
|
|
@@ -249,17 +277,23 @@ Exit 0 when every required check passes; optional warnings (an absent provider C
|
|
|
249
277
|
|
|
250
278
|
Migration: `loki modernize heal <repo> --assess` was a legacy command for read-only analysis. The v10 path `loki modernize <repo> --to <target>` is not yet finished: only `--dry-run` works (inventory, dependency graph, unit clustering, cost, time and risk estimate). A real run stops after the estimate and prints `modernize: oracle capture and execution are not built yet; use --dry-run`. Targets `python3` and `java21` exist; the Java dependency graph is not wired in yet. Design: [docs/v10/MODERNIZE.md](docs/v10/MODERNIZE.md), user summary: [docs/v10/GUIDE-MODERNIZE.md](docs/v10/GUIDE-MODERNIZE.md).
|
|
251
279
|
|
|
252
|
-
## Control Plane v0
|
|
280
|
+
## Control Plane v0
|
|
253
281
|
|
|
254
|
-
A local control plane and UI for many runs,
|
|
282
|
+
A local control plane and UI for many runs, on by default. Turn it off with `LOKI_CONTROL=0`:
|
|
255
283
|
|
|
256
284
|
```bash
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
285
|
+
loki control serve # 127.0.0.1, default port 47821 (--port N, --db PATH)
|
|
286
|
+
loki control backfill . # ship ./.loki/runs to the control plane
|
|
287
|
+
loki control status # reachable? how many runs held?
|
|
260
288
|
```
|
|
261
289
|
|
|
262
|
-
The URL comes from `LOKI_CONTROL_URL`, else `http://127.0.0.1:${LOKI_CONTROL_PORT:-47821}`.
|
|
290
|
+
The URL comes from `LOKI_CONTROL_URL`, else `http://127.0.0.1:${LOKI_CONTROL_PORT:-47821}`. While `loki control serve` is running, runs on this machine ship to it automatically (it publishes `~/.loki/control/instance.json`, mode 0600, removed on exit); a run never starts the server. Set `LOKI_CONTROL_URL` to ship elsewhere. See [docs/v10/CONTROL-PLANE.md](docs/v10/CONTROL-PLANE.md).
|
|
291
|
+
|
|
292
|
+
Answering a BLOCKED run: when a run stops on a spec conflict, its run page shows the question and an answer box. Submitting posts to `POST /v1/runs/<source>/<run>/answer` (JSON `{"answer": "..."}`, up to 4000 characters, loopback only) and writes `~/.loki/control/answers/<source>/<run>.answer.txt` (override the directory with `LOKI_CONTROL_ANSWER_DIR`). The page prints the resume command, `loki answer <run>`, which starts a fresh run carrying the task, the question and that file's text (or pass `--text "..."` yourself; with no run id it picks the newest BLOCKED run). A run that is not BLOCKED exits 2.
|
|
293
|
+
|
|
294
|
+
Set `LOKI_CONTROL_DEFAULT=1` (off by default) to make `loki dashboard`, `loki dashboard start` and `loki dashboard open` start the Control Plane instead of the old dashboard. The old dashboard is unchanged when the flag is unset.
|
|
295
|
+
|
|
296
|
+
Container, Helm and ECS deployment: [docs/control-plane-container.md](docs/control-plane-container.md).
|
|
263
297
|
|
|
264
298
|
## Providers
|
|
265
299
|
|
|
@@ -515,6 +549,7 @@ Loki 10 engine:
|
|
|
515
549
|
| `loki owner/repo#N` | Same, from a GitHub, GitLab or Jira issue |
|
|
516
550
|
| `loki quick "<task>"` | Small task, lean path, no PR |
|
|
517
551
|
| `loki backlog owner/repo --all\|--label X\|--issues N,N` | Run every matching open issue, N in parallel (`--concurrency N`, `--dry-run`) |
|
|
552
|
+
| `loki workspace list \| run <name> <ref>` | Experimental (`LOKI_WORKSPACES=1`): run one issue across the repos of a `loki.yaml` workspace; see [docs/WORKSPACES.md](docs/WORKSPACES.md) |
|
|
518
553
|
| `loki status [--json]` | Current status |
|
|
519
554
|
| `loki verify [run-id]` | Re-check a sealed receipt (exit codes above) |
|
|
520
555
|
| `loki doctor [--json] [--airgap]` | Check environment and providers |
|
|
@@ -611,7 +646,7 @@ loki enterprise status
|
|
|
611
646
|
| **Testing** | 8 automated quality gates | Test quality depends on AI assertions |
|
|
612
647
|
| **Providers** | Claude, Cline, Codex, Aider and opencode | Non-Claude providers are experimental and mostly sequential |
|
|
613
648
|
| **Dashboard** | Real-time single-machine monitoring | No multi-node clustering |
|
|
614
|
-
| **Loki 10** | The v10 run (`loki "<task>"`, `loki owner/repo#N`, `loki quick`), `status`, `verify`, `backlog` | `loki start <issue ref|issue URL|"task">` runs Loki 10, `loki start <file>` stays legacy; `loki modernize <repo> --to` runs `--dry-run` only; Slack is
|
|
649
|
+
| **Loki 10** | The v10 run (`loki "<task>"`, `loki owner/repo#N`, `loki quick`), `status`, `verify`, `backlog` | `loki start <issue ref|issue URL|"task">` runs Loki 10, `loki start <file>` stays legacy; `loki modernize <repo> --to` runs `--dry-run` only; two-way Slack is on by default (`loki slack serve`, `LOKI_SLACK_INBOUND=0` disables); Control Plane v0 is a preview |
|
|
615
650
|
|
|
616
651
|
> **What "autonomous" means:** the system runs RARV cycles without prompting. It does NOT access your cloud accounts, payment systems or external services unless you provide credentials. Human oversight is expected for deployment, API keys and critical decisions.
|
|
617
652
|
|
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 v10.6.
|
|
6
|
+
# Loki Mode v10.6.9
|
|
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 legacy engine, which still ships and is being removed (`loki start owner/repo#N` still routes to it; prefer `loki owner/repo#N`).
|
|
9
9
|
|
|
@@ -473,4 +473,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
473
473
|
|
|
474
474
|
---
|
|
475
475
|
|
|
476
|
-
**v10.6.
|
|
476
|
+
**v10.6.9 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
10.6.
|
|
1
|
+
10.6.9
|
package/autonomy/lib/backlog.py
CHANGED
|
@@ -10,6 +10,15 @@ ladder: 0 VERIFIED, 1 FAILED, 3 BUDGET_STOP, 4 BLOCKED, 5 STALLED). A draft PR
|
|
|
10
10
|
from a failed run is still FAILED. Exit: 0 all ok, 3 only budget stops, 1 other
|
|
11
11
|
failures, 2 usage/config error.
|
|
12
12
|
|
|
13
|
+
Unit mode (D61 slice 10): --dag FILE [--group G] also runs the units of a
|
|
14
|
+
decomposer DAG (engine10 Dag JSON: units[] with id, items, writeSet, optional
|
|
15
|
+
deps; edges[] {from,to} mean "to" waits for "from"). Unit n runs on branch
|
|
16
|
+
loki/unit-<group>-<n> in worktree unit-<group>-<n>, shares the one slot pool
|
|
17
|
+
with issues, starts only after every parent passed, and is SKIPPED when a
|
|
18
|
+
parent did not. The launcher gets the unit task text plus --no-pr, and the env
|
|
19
|
+
LOKI_GROUP_ID, LOKI_UNIT_ID, LOKI_UNIT_N, LOKI_UNIT_DEPS, LOKI_UNIT_WRITE_SET.
|
|
20
|
+
Passing unit branches are kept for the integrator; issue mode is unchanged.
|
|
21
|
+
|
|
13
22
|
Test seam: LOKI_BACKLOG_LAUNCHER replaces the real launcher (called with the
|
|
14
23
|
issue ref, cwd = the issue worktree).
|
|
15
24
|
"""
|
|
@@ -27,6 +36,8 @@ import time
|
|
|
27
36
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
28
37
|
sys.path.insert(0, HERE)
|
|
29
38
|
import loki_yaml # noqa: E402
|
|
39
|
+
import workspace # noqa: E402
|
|
40
|
+
import worktree_prep # noqa: E402
|
|
30
41
|
|
|
31
42
|
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
32
43
|
REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
@@ -110,6 +121,72 @@ def parse_result(rc, text):
|
|
|
110
121
|
return False, "FAILED: " + (reason or "exit %d" % rc), usd
|
|
111
122
|
|
|
112
123
|
|
|
124
|
+
MAX_DAG_UNITS = 500
|
|
125
|
+
MAX_DAG_EDGES = 5000
|
|
126
|
+
UNIT_ID_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.:/-]{0,127}$")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def load_dag(path, group):
|
|
130
|
+
"""-> (group, units) where units = [{n, id, task, deps(list of ids), write_set}] or raises ValueError."""
|
|
131
|
+
try:
|
|
132
|
+
with open(path) as f:
|
|
133
|
+
dag = json.load(f)
|
|
134
|
+
except (OSError, ValueError) as e:
|
|
135
|
+
raise ValueError("cannot read DAG %s: %s" % (path, e))
|
|
136
|
+
raw = dag.get("units") if isinstance(dag, dict) else None
|
|
137
|
+
if not isinstance(raw, list) or not raw:
|
|
138
|
+
raise ValueError("DAG has no units")
|
|
139
|
+
group = group or str(dag.get("group") or "g1")
|
|
140
|
+
if not re.match(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$", group) or ".." in group:
|
|
141
|
+
raise ValueError("group must match [A-Za-z0-9_][A-Za-z0-9_.-]* with no '..', got %r" % group)
|
|
142
|
+
if len(raw) > MAX_DAG_UNITS:
|
|
143
|
+
raise ValueError("DAG has %d units, the limit is %d" % (len(raw), MAX_DAG_UNITS))
|
|
144
|
+
ids = []
|
|
145
|
+
for u in raw:
|
|
146
|
+
uid = str(u.get("id", "")) if isinstance(u, dict) else ""
|
|
147
|
+
if not uid or uid in ids:
|
|
148
|
+
raise ValueError("DAG unit ids must be present and unique (got %r)" % uid)
|
|
149
|
+
if not UNIT_ID_RE.match(uid):
|
|
150
|
+
raise ValueError("DAG unit id %r is invalid (allowed: %s)" % (uid, UNIT_ID_RE.pattern))
|
|
151
|
+
ids.append(uid)
|
|
152
|
+
deps = {i: [] for i in ids}
|
|
153
|
+
for u in raw:
|
|
154
|
+
for d in u.get("deps") or []:
|
|
155
|
+
deps[str(u["id"])].append(str(d))
|
|
156
|
+
edges = dag.get("edges") or []
|
|
157
|
+
if not isinstance(edges, list) or len(edges) > MAX_DAG_EDGES:
|
|
158
|
+
raise ValueError("DAG edges must be a list of at most %d entries" % MAX_DAG_EDGES)
|
|
159
|
+
for e in edges:
|
|
160
|
+
if not isinstance(e, dict):
|
|
161
|
+
raise ValueError("DAG edge must be an object, got %r" % (e,))
|
|
162
|
+
src, dst = str(e.get("from")), str(e.get("to"))
|
|
163
|
+
if src not in deps or dst not in deps:
|
|
164
|
+
raise ValueError("DAG edge %s -> %s names an unknown unit id" % (src, dst))
|
|
165
|
+
if src not in deps[dst]:
|
|
166
|
+
deps[dst].append(src)
|
|
167
|
+
for i, ds in deps.items():
|
|
168
|
+
for d in ds:
|
|
169
|
+
if d not in deps or d == i:
|
|
170
|
+
raise ValueError("unit %s depends on unknown or itself: %s" % (i, d))
|
|
171
|
+
left = dict(deps)
|
|
172
|
+
while left:
|
|
173
|
+
free = [i for i, ds in left.items() if not [d for d in ds if d in left]]
|
|
174
|
+
if not free:
|
|
175
|
+
raise ValueError("DAG has a dependency cycle among: " + ", ".join(sorted(left)))
|
|
176
|
+
for i in free:
|
|
177
|
+
del left[i]
|
|
178
|
+
units = []
|
|
179
|
+
for n, u in enumerate(raw, 1):
|
|
180
|
+
uid = str(u["id"])
|
|
181
|
+
items = u.get("items") or []
|
|
182
|
+
task = "\n".join(str(x) for x in items) or str(u.get("task") or uid)
|
|
183
|
+
if task.lstrip().startswith("-"):
|
|
184
|
+
raise ValueError("unit %s task text starts with '-' and would be read as an option" % uid)
|
|
185
|
+
units.append({"n": n, "id": uid, "deps": deps[uid], "write_set": list(u.get("writeSet") or []),
|
|
186
|
+
"task": task})
|
|
187
|
+
return group, units
|
|
188
|
+
|
|
189
|
+
|
|
113
190
|
def main(argv):
|
|
114
191
|
ap = argparse.ArgumentParser(prog="loki backlog", add_help=True)
|
|
115
192
|
ap.add_argument("repo")
|
|
@@ -119,12 +196,20 @@ def main(argv):
|
|
|
119
196
|
sel.add_argument("--issues")
|
|
120
197
|
ap.add_argument("--concurrency", type=int)
|
|
121
198
|
ap.add_argument("--dry-run", action="store_true")
|
|
199
|
+
ap.add_argument("--dag", help="decomposer DAG JSON; its units run on loki/unit-<group>-<n>")
|
|
200
|
+
ap.add_argument("--group", help="unit group id (default: the DAG's group, else g1)")
|
|
122
201
|
a = ap.parse_args(argv)
|
|
123
202
|
|
|
124
203
|
if not REPO_RE.match(a.repo):
|
|
125
204
|
return die("repo must be owner/repo, got %r" % a.repo)
|
|
126
|
-
if not (a.all or a.label or a.issues):
|
|
127
|
-
return die("choose one of --all, --label X, --issues 1,2,3")
|
|
205
|
+
if not (a.all or a.label or a.issues or a.dag):
|
|
206
|
+
return die("choose one of --all, --label X, --issues 1,2,3, --dag FILE")
|
|
207
|
+
group, dag_units = None, []
|
|
208
|
+
if a.dag:
|
|
209
|
+
try:
|
|
210
|
+
group, dag_units = load_dag(a.dag, a.group)
|
|
211
|
+
except ValueError as e:
|
|
212
|
+
return die(str(e))
|
|
128
213
|
numbers = None
|
|
129
214
|
if a.issues:
|
|
130
215
|
if not re.match(r"^\d+(,\d+)*$", a.issues):
|
|
@@ -162,16 +247,19 @@ def main(argv):
|
|
|
162
247
|
|
|
163
248
|
try:
|
|
164
249
|
issues = ([{"number": n, "title": ""} for n in numbers] if numbers
|
|
165
|
-
else list_issues(a.repo, a.label, env))
|
|
250
|
+
else list_issues(a.repo, a.label, env) if (a.all or a.label) else [])
|
|
166
251
|
except RuntimeError as e:
|
|
167
252
|
return die(redact(str(e), token), 1)
|
|
168
253
|
|
|
169
|
-
say("backlog: %s concurrency %d, %d issue(s)" % (a.repo, conc, len(issues))
|
|
254
|
+
say("backlog: %s concurrency %d, %d issue(s)" % (a.repo, conc, len(issues))
|
|
255
|
+
+ (", %d unit(s) in group %s" % (len(dag_units), group) if dag_units else ""))
|
|
170
256
|
if a.dry_run:
|
|
171
257
|
for i in issues:
|
|
172
258
|
say("backlog: would run #%d %s" % (i["number"], i["title"]))
|
|
259
|
+
for u in dag_units:
|
|
260
|
+
say("backlog: would run unit %s-%d (%s) after: %s" % (group, u["n"], u["id"], ", ".join(u["deps"]) or "nothing"))
|
|
173
261
|
return 0
|
|
174
|
-
if not issues:
|
|
262
|
+
if not issues and not dag_units:
|
|
175
263
|
say("backlog: nothing to do")
|
|
176
264
|
return 0
|
|
177
265
|
|
|
@@ -191,11 +279,25 @@ def main(argv):
|
|
|
191
279
|
child_env["LOKI_BUDGET_LIMIT"] = str(cfg["budgets"]["per_run_usd"])
|
|
192
280
|
day_cap = cfg.get("budgets", {}).get("per_day_usd")
|
|
193
281
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
282
|
+
# One job table for issues and units: they share one slot pool.
|
|
283
|
+
spec = {}
|
|
284
|
+
for i in issues:
|
|
285
|
+
n = i["number"]
|
|
286
|
+
spec[n] = {"label": "#%d" % n, "branch": "loki/backlog-%d" % n, "wt": "issue-%d" % n,
|
|
287
|
+
"cmd": [launcher, "%s#%d" % (a.repo, n)], "env": {}, "deps": [], "unit": False}
|
|
288
|
+
key_of = {u["id"]: "unit-%s-%d" % (group, u["n"]) for u in dag_units}
|
|
289
|
+
for u in dag_units:
|
|
290
|
+
k = key_of[u["id"]]
|
|
291
|
+
spec[k] = {"label": k, "branch": "loki/" + k, "wt": k, "unit": True,
|
|
292
|
+
"cmd": [launcher, u["task"], "--no-pr"],
|
|
293
|
+
"deps": [key_of[d] for d in u["deps"]],
|
|
294
|
+
"env": {"LOKI_GROUP_ID": group, "LOKI_UNIT_ID": u["id"], "LOKI_UNIT_N": str(u["n"]),
|
|
295
|
+
"LOKI_UNIT_DEPS": ",".join(u["deps"]), "LOKI_UNIT_WRITE_SET": "\n".join(u["write_set"])}}
|
|
296
|
+
state = {k: {"status": "queued", "ok": None, "cost": None, "ran": False} for k in spec}
|
|
297
|
+
for k in state:
|
|
298
|
+
say("backlog: %s queued" % spec[k]["label"])
|
|
299
|
+
pending = list(spec)
|
|
300
|
+
running = {} # key -> (Popen, log path, worktree)
|
|
199
301
|
unmeasured = 0
|
|
200
302
|
|
|
201
303
|
def stop_children(*_):
|
|
@@ -208,9 +310,9 @@ def main(argv):
|
|
|
208
310
|
signal.signal(signal.SIGTERM, stop_children)
|
|
209
311
|
signal.signal(signal.SIGINT, stop_children)
|
|
210
312
|
|
|
211
|
-
def finish(
|
|
313
|
+
def finish(k):
|
|
212
314
|
nonlocal unmeasured
|
|
213
|
-
p, log, wt = running.pop(
|
|
315
|
+
p, log, wt = running.pop(k)
|
|
214
316
|
try:
|
|
215
317
|
with open(log, errors="replace") as f:
|
|
216
318
|
text = redact(f.read(), token)
|
|
@@ -223,51 +325,80 @@ def main(argv):
|
|
|
223
325
|
unmeasured += 1
|
|
224
326
|
else:
|
|
225
327
|
add_spend(usd)
|
|
226
|
-
state[
|
|
227
|
-
say("backlog:
|
|
328
|
+
state[k].update(status=status, ok=ok, cost=usd)
|
|
329
|
+
say("backlog: %s %s" % (spec[k]["label"], status))
|
|
228
330
|
if ok:
|
|
229
331
|
git("worktree", "remove", "--force", wt, cwd=top)
|
|
230
|
-
|
|
332
|
+
if not spec[k]["unit"]: # a passing unit branch is kept for the integrator
|
|
333
|
+
git("branch", "-D", spec[k]["branch"], cwd=top)
|
|
231
334
|
|
|
232
|
-
def launch(
|
|
233
|
-
|
|
335
|
+
def launch(k):
|
|
336
|
+
s = spec[k]
|
|
337
|
+
wt = os.path.join(wt_root, s["wt"])
|
|
234
338
|
if os.path.exists(wt):
|
|
235
339
|
git("worktree", "remove", "--force", wt, cwd=top)
|
|
236
|
-
|
|
237
|
-
|
|
340
|
+
if workspace.enabled():
|
|
341
|
+
# Shared worktree prep (D51-B05): per-base lock, clean start point, deps copied.
|
|
342
|
+
git("branch", "-D", s["branch"], cwd=top)
|
|
343
|
+
try:
|
|
344
|
+
worktree_prep.prepare_worktree(top, wt, s["branch"])
|
|
345
|
+
r = None
|
|
346
|
+
except (RuntimeError, subprocess.CalledProcessError) as e:
|
|
347
|
+
msg = getattr(e, "stderr", None) or str(e)
|
|
348
|
+
r = subprocess.CompletedProcess([], 1, "", msg)
|
|
349
|
+
else:
|
|
350
|
+
r = git("worktree", "add", "-B", s["branch"], wt, "HEAD", cwd=top)
|
|
351
|
+
if r is not None and r.returncode != 0:
|
|
238
352
|
err = (r.stderr.strip().splitlines() or ["git error"])[-1]
|
|
239
|
-
state[
|
|
240
|
-
say("backlog:
|
|
353
|
+
state[k].update(status="FAILED: worktree: " + redact(err, token), ok=False)
|
|
354
|
+
say("backlog: %s %s" % (s["label"], state[k]["status"]))
|
|
241
355
|
return
|
|
242
|
-
log = os.path.join(wt_root, "logs", "
|
|
243
|
-
cmd = [launcher, "%s#%d" % (a.repo, n)]
|
|
356
|
+
log = os.path.join(wt_root, "logs", s["wt"] + ".log")
|
|
244
357
|
with open(log, "w") as lf:
|
|
245
|
-
p = subprocess.Popen(cmd, cwd=wt, env=child_env, stdout=lf,
|
|
246
|
-
stdin=subprocess.DEVNULL, start_new_session=True)
|
|
247
|
-
running[
|
|
248
|
-
state[
|
|
249
|
-
say("backlog:
|
|
358
|
+
p = subprocess.Popen(s["cmd"], cwd=wt, env=dict(child_env, **s["env"]), stdout=lf,
|
|
359
|
+
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
|
|
360
|
+
running[k] = (p, log, wt)
|
|
361
|
+
state[k].update(status="running", ran=True)
|
|
362
|
+
say("backlog: %s running" % s["label"])
|
|
363
|
+
|
|
364
|
+
def next_ready():
|
|
365
|
+
"""First pending job whose parents all passed; skip jobs whose parent did not."""
|
|
366
|
+
for k in list(pending):
|
|
367
|
+
ds = spec[k]["deps"]
|
|
368
|
+
bad = [d for d in ds if state[d]["ok"] is False]
|
|
369
|
+
if bad:
|
|
370
|
+
pending.remove(k)
|
|
371
|
+
state[k].update(status="SKIPPED: dependency %s did not pass" % spec[bad[0]]["label"], ok=False)
|
|
372
|
+
say("backlog: %s %s" % (spec[k]["label"], state[k]["status"]))
|
|
373
|
+
return next_ready()
|
|
374
|
+
if all(state[d]["ok"] is True for d in ds):
|
|
375
|
+
pending.remove(k)
|
|
376
|
+
return k
|
|
377
|
+
return None
|
|
250
378
|
|
|
251
379
|
while pending or running:
|
|
252
|
-
for
|
|
253
|
-
finish(
|
|
380
|
+
for k in [k for k, (p, _l, _w) in running.items() if p.poll() is not None]:
|
|
381
|
+
finish(k)
|
|
254
382
|
while pending and len(running) < conc:
|
|
255
383
|
if day_cap and spent_today() >= day_cap:
|
|
256
|
-
for
|
|
257
|
-
state[
|
|
258
|
-
say("backlog:
|
|
384
|
+
for k in pending:
|
|
385
|
+
state[k].update(status="BUDGET_STOP: daily budget $%.2f reached ($%.2f spent)" % (day_cap, spent_today()), ok=False)
|
|
386
|
+
say("backlog: %s %s" % (spec[k]["label"], state[k]["status"]))
|
|
259
387
|
pending = []
|
|
260
388
|
break
|
|
261
|
-
|
|
389
|
+
k = next_ready()
|
|
390
|
+
if k is None:
|
|
391
|
+
break
|
|
392
|
+
launch(k)
|
|
262
393
|
if running:
|
|
263
394
|
time.sleep(0.2)
|
|
264
395
|
|
|
265
396
|
# ---- summary -----------------------------------------------------------
|
|
266
397
|
say("")
|
|
267
398
|
say("Summary: %s" % a.repo)
|
|
268
|
-
say("%-
|
|
399
|
+
say("%-14s %-10s %s" % ("ITEM", "COST", "RESULT"))
|
|
269
400
|
for n, s in state.items():
|
|
270
|
-
say("%-
|
|
401
|
+
say("%-14s %-10s %s" % (spec[n]["label"], ("$%.2f" % s["cost"] if s["cost"] is not None else "unmeasured" if s["ran"] else "-"), s["status"]))
|
|
271
402
|
measured = sum(s["cost"] or 0 for s in state.values())
|
|
272
403
|
say("Total measured cost: $%.2f%s" % (measured, " (%d run(s) unmeasured, not counted as $0)" % unmeasured if unmeasured else ""))
|
|
273
404
|
bad = [s for s in state.values() if not s["ok"]]
|