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.
Files changed (113) hide show
  1. package/README.md +44 -9
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/backlog.py +167 -36
  5. package/autonomy/lib/workspace.py +384 -0
  6. package/autonomy/lib/workspace_comment.py +91 -0
  7. package/autonomy/lib/worktree_prep.py +143 -0
  8. package/autonomy/loki +96 -3
  9. package/bin/loki +3 -3
  10. package/completions/_loki +2 -0
  11. package/completions/loki.bash +1 -2
  12. package/dashboard/__init__.py +1 -1
  13. package/dashboard/api_start.py +26 -1
  14. package/docs/INSTALLATION.md +1 -1
  15. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  16. package/docs/WORKSPACES.md +36 -0
  17. package/docs/alternative-installations.md +3 -1
  18. package/docs/control-plane-container.md +50 -0
  19. package/docs/dashboard-guide.md +4 -0
  20. package/docs/dev/architecture-reference.md +4 -0
  21. package/docs/dev/project-structure.md +1 -1
  22. package/docs/exit-codes.md +1 -0
  23. package/docs/slack.md +34 -0
  24. package/docs/trackers.md +27 -0
  25. package/docs/v10/BOARD.md +74 -14
  26. package/docs/v10/CONTROL-PLANE.md +17 -6
  27. package/docs/v10/D51-PHASE-B.md +3 -1
  28. package/docs/v10/D61-SPEED.md +11 -2
  29. package/docs/v10/D63-CARDS.md +354 -0
  30. package/docs/v10/DECISIONS.md +63 -0
  31. package/docs/v10/DEPS.md +6 -2
  32. package/docs/v10/E2E-D65.md +87 -0
  33. package/docs/v10/HANDOFF-NEXT-WEEK.md +10 -4
  34. package/docs/v10/PROGRESS.md +63 -0
  35. package/docs/v10/ROADMAP-D63.md +35 -0
  36. package/loki-ts/dist/loki.js +612 -556
  37. package/mcp/__init__.py +1 -1
  38. package/mcp/server.py +76 -0
  39. package/mcp/tests/test_v10_tools.py +193 -0
  40. package/mcp/v10_tools.py +182 -0
  41. package/package.json +1 -1
  42. package/packages/control-plane/dist/server.js +131 -9
  43. package/packages/control-plane/drizzle/0001_group_id.sql +3 -0
  44. package/packages/control-plane/drizzle/meta/_journal.json +8 -1
  45. package/packages/control-plane/src/db/schema.ts +3 -0
  46. package/packages/control-plane/src/server/answer.ts +33 -0
  47. package/packages/control-plane/src/server/app.ts +22 -2
  48. package/packages/control-plane/src/server/auth.ts +51 -0
  49. package/packages/control-plane/src/server/runs.ts +6 -3
  50. package/packages/control-plane/src/server/serve.ts +13 -3
  51. package/packages/control-plane/src/shipper/discover.ts +29 -0
  52. package/packages/control-plane/ui/dist/assets/{index-BH3LtKcG.css → index-CFfrqCx2.css} +1 -1
  53. package/packages/control-plane/ui/dist/assets/index-Jt31nrK-.js +114 -0
  54. package/packages/control-plane/ui/dist/index.html +2 -2
  55. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  56. package/web-app/dist/assets/{AdminPage-DyjnUcaP.js → AdminPage-CZKZLuWU.js} +1 -1
  57. package/web-app/dist/assets/{Avatar-CY0sQNao.js → Avatar-Dcy9kO2E.js} +1 -1
  58. package/web-app/dist/assets/{Badge-BmwTRu72.js → Badge-DUotUSyx.js} +1 -1
  59. package/web-app/dist/assets/{Button-BRf5aTlb.js → Button-CFz_zfMZ.js} +1 -1
  60. package/web-app/dist/assets/{CockpitPage-RR94HuxS.js → CockpitPage-BhRw_oiM.js} +1 -1
  61. package/web-app/dist/assets/{ComparePage-CFWAuPRO.js → ComparePage-DfnsRTTC.js} +1 -1
  62. package/web-app/dist/assets/{ErrorBoundary-4cDADrG-.js → ErrorBoundary-Dn0gUu3D.js} +1 -1
  63. package/web-app/dist/assets/{EvidenceReceiptPanel-ChPFx2eM.js → EvidenceReceiptPanel-Co-xi282.js} +1 -1
  64. package/web-app/dist/assets/{GitHubIssuesPanel-B0TAjyeW.js → GitHubIssuesPanel-CoCpfexq.js} +1 -1
  65. package/web-app/dist/assets/{GitHubPRsPanel-BbszEwW9.js → GitHubPRsPanel-Dg0Eg6F_.js} +1 -1
  66. package/web-app/dist/assets/{HomePage-DrBgzs1d.js → HomePage-BysxL4mV.js} +2 -2
  67. package/web-app/dist/assets/{LoginPage-Bu7b2DGz.js → LoginPage-CQ366p_d.js} +1 -1
  68. package/web-app/dist/assets/{MagicPage-BFEYp631.js → MagicPage-DD7YcbNq.js} +1 -1
  69. package/web-app/dist/assets/{MetricsPage-eIe2naVR.js → MetricsPage-BYz49xQj.js} +1 -1
  70. package/web-app/dist/assets/{NotFoundPage-BVWYkNt9.js → NotFoundPage-CKs09MeO.js} +1 -1
  71. package/web-app/dist/assets/{ProjectPage-zmxyu732.js → ProjectPage-Ci2BIA_D.js} +3 -3
  72. package/web-app/dist/assets/{ProjectsPage-Dq5NdCak.js → ProjectsPage-ClwMH0eu.js} +1 -1
  73. package/web-app/dist/assets/{SettingsPage-CCrJOMe0.js → SettingsPage-Cm0rCX-y.js} +1 -1
  74. package/web-app/dist/assets/{ShowcasePage-n-Ipzp01.js → ShowcasePage-CxvESt8X.js} +1 -1
  75. package/web-app/dist/assets/{SystemSettingsPage-CP_rjYXK.js → SystemSettingsPage-W_CGYPOY.js} +1 -1
  76. package/web-app/dist/assets/{TeamsPage-B_P9DepW.js → TeamsPage-feVvRvys.js} +1 -1
  77. package/web-app/dist/assets/{TemplatesPage-h-3A6PQT.js → TemplatesPage-C-pUWMmF.js} +1 -1
  78. package/web-app/dist/assets/{TerminalOutput-pD0nJ1BU.js → TerminalOutput-j5ibjKY3.js} +1 -1
  79. package/web-app/dist/assets/{activity-ChFwzT-d.js → activity-TTdVYMa_.js} +1 -1
  80. package/web-app/dist/assets/{bell-BX-eK8Gu.js → bell-DeoTFwpv.js} +1 -1
  81. package/web-app/dist/assets/{bot-fj5PkPtR.js → bot-B5SghxqU.js} +1 -1
  82. package/web-app/dist/assets/{check-BGAbHlvD.js → check-k8SzsdbE.js} +1 -1
  83. package/web-app/dist/assets/{chevron-left-BzFUxHgb.js → chevron-left-DHnEllFz.js} +1 -1
  84. package/web-app/dist/assets/{circle-alert-BQrg5DWl.js → circle-alert-DW_SLdcB.js} +1 -1
  85. package/web-app/dist/assets/{clock-CTu9lkVo.js → clock-Bmoaxy09.js} +1 -1
  86. package/web-app/dist/assets/{cloud-DrIENRiE.js → cloud-eg3tzXTh.js} +1 -1
  87. package/web-app/dist/assets/{code-xml-PaoVpfur.js → code-xml-VAfXihhD.js} +1 -1
  88. package/web-app/dist/assets/{database-DMmkAotn.js → database-CY3_-5aK.js} +1 -1
  89. package/web-app/dist/assets/{dollar-sign-B2LQdBoX.js → dollar-sign-DYC0OLlg.js} +1 -1
  90. package/web-app/dist/assets/{file-code-corner-Dfv1XXeR.js → file-code-corner-0X3SveKz.js} +1 -1
  91. package/web-app/dist/assets/{file-plus-D2Wzf2wd.js → file-plus-B_oh_ukl.js} +1 -1
  92. package/web-app/dist/assets/{globe-DxotVWt8.js → globe-F0tLA8WJ.js} +1 -1
  93. package/web-app/dist/assets/{hammer-Brfy5ZrJ.js → hammer-DrlvBmfW.js} +1 -1
  94. package/web-app/dist/assets/{index-BfhHMwi8.js → index-YihVaZAj.js} +3 -3
  95. package/web-app/dist/assets/{layers-DfxSWDvp.js → layers-CbJfn5UC.js} +1 -1
  96. package/web-app/dist/assets/{loader-circle-CxIrzNwx.js → loader-circle-CpmOggou.js} +1 -1
  97. package/web-app/dist/assets/{lock-m6nzhtpU.js → lock-BGJ6QWrI.js} +1 -1
  98. package/web-app/dist/assets/{package-ri9oE9Jc.js → package-BTmH4Xto.js} +1 -1
  99. package/web-app/dist/assets/{plus-DWeTAMVV.js → plus-mKQ7hz-8.js} +1 -1
  100. package/web-app/dist/assets/{refresh-cw-BGMSpaMh.js → refresh-cw-vPetpLsm.js} +1 -1
  101. package/web-app/dist/assets/{rotate-ccw-DD1xBD_b.js → rotate-ccw-DiJ8GFhj.js} +1 -1
  102. package/web-app/dist/assets/{scroll-text-BCCR_AN9.js → scroll-text-DpPQIlQR.js} +1 -1
  103. package/web-app/dist/assets/{server-CmMWPIjS.js → server-CiaV4drx.js} +1 -1
  104. package/web-app/dist/assets/{shield-alert-BZZmBVEJ.js → shield-alert-BaaQqyco.js} +1 -1
  105. package/web-app/dist/assets/{thumbs-up-OaxbNRXu.js → thumbs-up-2zNCKp0Q.js} +1 -1
  106. package/web-app/dist/assets/{trash-2-DmcSzTfr.js → trash-2-CiVDx5Co.js} +1 -1
  107. package/web-app/dist/assets/{trending-down-BmfXLykz.js → trending-down-tcxvw5BH.js} +1 -1
  108. package/web-app/dist/assets/{trending-up-Djec3jIq.js → trending-up-C2bv2_v_.js} +1 -1
  109. package/web-app/dist/assets/{upload-BN8dXIlF.js → upload-DS4VPUAp.js} +1 -1
  110. package/web-app/dist/assets/{usePolling-Cu4yi04I.js → usePolling-JggBcVln.js} +1 -1
  111. package/web-app/dist/assets/{user-IBI-k10o.js → user-CvavvOxM.js} +1 -1
  112. package/web-app/dist/index.html +1 -1
  113. 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 36 tools over stdio (`mcp/server.py`).
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 notifications are not part of the v10 engine surface yet.
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 (preview)
280
+ ## Control Plane v0
253
281
 
254
- A local control plane and UI for many runs, off by default. Enable it with `LOKI_CONTROL=1`:
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
- LOKI_CONTROL=1 loki control serve # 127.0.0.1, default port 47821 (--port N, --db PATH)
258
- LOKI_CONTROL=1 loki control backfill . # ship ./.loki/runs to the control plane
259
- LOKI_CONTROL=1 loki control status # reachable? how many runs held?
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}`. Set `LOKI_CONTROL_URL` on a run to ship it live. See [docs/v10/CONTROL-PLANE.md](docs/v10/CONTROL-PLANE.md).
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 not wired in; Control Plane v0 is a preview |
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.7
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.7 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
476
+ **v10.6.9 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 10.6.7
1
+ 10.6.9
@@ -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
- state = {i["number"]: {"status": "queued", "ok": None, "cost": None, "ran": False} for i in issues}
195
- for n in state:
196
- say("backlog: #%d queued" % n)
197
- pending = [i["number"] for i in issues]
198
- running = {} # n -> (Popen, log path, worktree)
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(n):
313
+ def finish(k):
212
314
  nonlocal unmeasured
213
- p, log, wt = running.pop(n)
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[n].update(status=status, ok=ok, cost=usd)
227
- say("backlog: #%d %s" % (n, status))
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
- git("branch", "-D", "loki/backlog-%d" % n, cwd=top)
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(n):
233
- wt = os.path.join(wt_root, "issue-%d" % n)
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
- r = git("worktree", "add", "-B", "loki/backlog-%d" % n, wt, "HEAD", cwd=top)
237
- if r.returncode != 0:
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[n].update(status="FAILED: worktree: " + redact(err, token), ok=False)
240
- say("backlog: #%d %s" % (n, state[n]["status"]))
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", "issue-%d.log" % n)
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, stderr=subprocess.STDOUT,
246
- stdin=subprocess.DEVNULL, start_new_session=True)
247
- running[n] = (p, log, wt)
248
- state[n].update(status="running", ran=True)
249
- say("backlog: #%d running" % n)
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 n in [n for n, (p, _l, _w) in running.items() if p.poll() is not None]:
253
- finish(n)
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 n in pending:
257
- state[n].update(status="BUDGET_STOP: daily budget $%.2f reached ($%.2f spent)" % (day_cap, spent_today()), ok=False)
258
- say("backlog: #%d %s" % (n, state[n]["status"]))
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
- launch(pending.pop(0))
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("%-8s %-8s %s" % ("ISSUE", "COST", "RESULT"))
399
+ say("%-14s %-10s %s" % ("ITEM", "COST", "RESULT"))
269
400
  for n, s in state.items():
270
- say("%-8s %-8s %s" % ("#%d" % n, ("$%.2f" % s["cost"] if s["cost"] is not None else "unmeasured" if s["ran"] else "-"), s["status"]))
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"]]