loki-mode 10.6.7 → 10.6.8
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 +35 -1
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/workspace.py +290 -0
- package/autonomy/lib/worktree_prep.py +143 -0
- package/autonomy/loki +25 -0
- package/bin/loki +1 -1
- package/completions/_loki +1 -0
- package/completions/loki.bash +1 -2
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
- package/docs/WORKSPACES.md +29 -0
- package/docs/alternative-installations.md +3 -1
- package/docs/control-plane-container.md +45 -0
- package/docs/dev/architecture-reference.md +4 -0
- package/docs/dev/project-structure.md +1 -1
- package/docs/slack.md +26 -0
- package/docs/v10/BOARD.md +26 -1
- package/docs/v10/CONTROL-PLANE.md +4 -0
- package/docs/v10/D61-SPEED.md +7 -2
- package/docs/v10/DECISIONS.md +24 -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 +13 -0
- package/docs/v10/ROADMAP-D63.md +35 -0
- package/loki-ts/dist/loki.js +528 -491
- package/mcp/__init__.py +1 -1
- package/mcp/server.py +76 -0
- package/mcp/tests/test_v10_tools.py +137 -0
- package/mcp/v10_tools.py +172 -0
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +56 -4
- package/packages/control-plane/src/server/answer.ts +33 -0
- package/packages/control-plane/src/server/app.ts +17 -1
- package/packages/control-plane/src/server/runs.ts +2 -0
- package/packages/control-plane/src/server/serve.ts +2 -2
- package/packages/control-plane/ui/dist/assets/index-53E32yrO.js +114 -0
- package/packages/control-plane/ui/dist/assets/{index-BH3LtKcG.css → index-CFfrqCx2.css} +1 -1
- 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-DN_pe8vo.js} +1 -1
- package/web-app/dist/assets/{Avatar-CY0sQNao.js → Avatar-BnQsr8z-.js} +1 -1
- package/web-app/dist/assets/{Badge-BmwTRu72.js → Badge-DzERiZn2.js} +1 -1
- package/web-app/dist/assets/{Button-BRf5aTlb.js → Button-CaaoxQAt.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-RR94HuxS.js → CockpitPage-SXhnysJA.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CFWAuPRO.js → ComparePage-krxTCFJY.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-4cDADrG-.js → ErrorBoundary-BL96ESMc.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-ChPFx2eM.js → EvidenceReceiptPanel-PUiGSpkB.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-B0TAjyeW.js → GitHubIssuesPanel-C0hPs-P2.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-BbszEwW9.js → GitHubPRsPanel-CLvqNUlu.js} +1 -1
- package/web-app/dist/assets/{HomePage-DrBgzs1d.js → HomePage-X-sq_Rc9.js} +2 -2
- package/web-app/dist/assets/{LoginPage-Bu7b2DGz.js → LoginPage-DhQuwdua.js} +1 -1
- package/web-app/dist/assets/{MagicPage-BFEYp631.js → MagicPage-CP2ODjfn.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-eIe2naVR.js → MetricsPage-C6SYiQES.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-BVWYkNt9.js → NotFoundPage-CCDxxYc-.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-zmxyu732.js → ProjectPage-yDF--8FQ.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-Dq5NdCak.js → ProjectsPage-BBLspdLn.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-CCrJOMe0.js → SettingsPage-C_sWbQRw.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-n-Ipzp01.js → ShowcasePage-xCMuL0N8.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-CP_rjYXK.js → SystemSettingsPage-DnJKGCx7.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B_P9DepW.js → TeamsPage-BBXdhrSs.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-h-3A6PQT.js → TemplatesPage-AZ1Lr1rn.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-pD0nJ1BU.js → TerminalOutput-CtIm9tXc.js} +1 -1
- package/web-app/dist/assets/{activity-ChFwzT-d.js → activity-Dk1FK25Q.js} +1 -1
- package/web-app/dist/assets/{bell-BX-eK8Gu.js → bell-dhqXyzP4.js} +1 -1
- package/web-app/dist/assets/{bot-fj5PkPtR.js → bot-CExHd4Kf.js} +1 -1
- package/web-app/dist/assets/{check-BGAbHlvD.js → check-D51zaA1a.js} +1 -1
- package/web-app/dist/assets/{chevron-left-BzFUxHgb.js → chevron-left-DEwnQzH1.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BQrg5DWl.js → circle-alert-C6U90Msk.js} +1 -1
- package/web-app/dist/assets/{clock-CTu9lkVo.js → clock-6mFbMzzH.js} +1 -1
- package/web-app/dist/assets/{cloud-DrIENRiE.js → cloud-BrZZtE7F.js} +1 -1
- package/web-app/dist/assets/{code-xml-PaoVpfur.js → code-xml-DTFzeepP.js} +1 -1
- package/web-app/dist/assets/{database-DMmkAotn.js → database-B5rEiTre.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-B2LQdBoX.js → dollar-sign-bQs4rlS_.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-Dfv1XXeR.js → file-code-corner-Mo6e6qbf.js} +1 -1
- package/web-app/dist/assets/{file-plus-D2Wzf2wd.js → file-plus-DzHZ0jpP.js} +1 -1
- package/web-app/dist/assets/{globe-DxotVWt8.js → globe-DYN9UhTa.js} +1 -1
- package/web-app/dist/assets/{hammer-Brfy5ZrJ.js → hammer-bB0hSRcJ.js} +1 -1
- package/web-app/dist/assets/{index-BfhHMwi8.js → index-CIH5CqZB.js} +3 -3
- package/web-app/dist/assets/{layers-DfxSWDvp.js → layers-qhFkY8Nu.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CxIrzNwx.js → loader-circle-BGBkaYH7.js} +1 -1
- package/web-app/dist/assets/{lock-m6nzhtpU.js → lock-DIcsNFum.js} +1 -1
- package/web-app/dist/assets/{package-ri9oE9Jc.js → package-BdIIbZhS.js} +1 -1
- package/web-app/dist/assets/{plus-DWeTAMVV.js → plus-Co1Bv8zT.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-BGMSpaMh.js → refresh-cw-CGF4ABWH.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-DD1xBD_b.js → rotate-ccw-BX10Q79h.js} +1 -1
- package/web-app/dist/assets/{scroll-text-BCCR_AN9.js → scroll-text-CbXDlpGp.js} +1 -1
- package/web-app/dist/assets/{server-CmMWPIjS.js → server-CIhi6yUO.js} +1 -1
- package/web-app/dist/assets/{shield-alert-BZZmBVEJ.js → shield-alert-DJ5fB6Dg.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-OaxbNRXu.js → thumbs-up-BEft2T94.js} +1 -1
- package/web-app/dist/assets/{trash-2-DmcSzTfr.js → trash-2-CWGUPn7Y.js} +1 -1
- package/web-app/dist/assets/{trending-down-BmfXLykz.js → trending-down-CjqstvxQ.js} +1 -1
- package/web-app/dist/assets/{trending-up-Djec3jIq.js → trending-up-CTyLjMzE.js} +1 -1
- package/web-app/dist/assets/{upload-BN8dXIlF.js → upload-BmRx-uv4.js} +1 -1
- package/web-app/dist/assets/{usePolling-Cu4yi04I.js → usePolling-ChHkSJye.js} +1 -1
- package/web-app/dist/assets/{user-IBI-k10o.js → user-ZXh_f35z.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.
|
|
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
|
```
|
|
@@ -153,6 +155,27 @@ A finished run prints a short summary (see [Quiet output](#quiet-output), which
|
|
|
153
155
|
|
|
154
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`. Slack notifications are not part of the v10 engine surface yet.
|
|
155
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
|
+
|
|
171
|
+
## Two-way Slack (preview)
|
|
172
|
+
|
|
173
|
+
With `LOKI_SLACK_INBOUND=1`, `loki slack serve --port N` (127.0.0.1 by default) lets you mention `@loki <issue ref or task>` in Slack to start a run, and answer a BLOCKED question in the same thread. Needs `SLACK_BOT_TOKEN` and `SLACK_SIGNING_SECRET` in the environment. See [docs/slack.md](docs/slack.md) for the scopes.
|
|
174
|
+
|
|
175
|
+
## Spec to contract
|
|
176
|
+
|
|
177
|
+
Opt in with `LOKI_CONTRACT=1` (off by default). `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.
|
|
178
|
+
|
|
156
179
|
## Triggers without a cloud
|
|
157
180
|
|
|
158
181
|
Both run on your own GitHub Actions minutes; there is no hosted service.
|
|
@@ -210,6 +233,10 @@ loki verify
|
|
|
210
233
|
|
|
211
234
|
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
235
|
|
|
236
|
+
### Visual evidence (opt-in)
|
|
237
|
+
|
|
238
|
+
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.
|
|
239
|
+
|
|
213
240
|
## Quiet output
|
|
214
241
|
|
|
215
242
|
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.
|
|
@@ -261,6 +288,12 @@ LOKI_CONTROL=1 loki control status # reachable? how many runs held?
|
|
|
261
288
|
|
|
262
289
|
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).
|
|
263
290
|
|
|
291
|
+
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, which re-runs the task with that file's text.
|
|
292
|
+
|
|
293
|
+
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.
|
|
294
|
+
|
|
295
|
+
Container, Helm and ECS deployment: [docs/control-plane-container.md](docs/control-plane-container.md).
|
|
296
|
+
|
|
264
297
|
## Providers
|
|
265
298
|
|
|
266
299
|
Loki's autonomy and quality loop are the product; the coding CLI is swappable. With `LOKI_PROVIDER` unset, Loki auto-detects the first installed provider in this order (`providers/loader.sh`): claude, cline, codex, aider, opencode. An explicit choice always wins and is never silently substituted. Gemini CLI is deprecated: `LOKI_PROVIDER=gemini` exits with a migration message.
|
|
@@ -515,6 +548,7 @@ Loki 10 engine:
|
|
|
515
548
|
| `loki owner/repo#N` | Same, from a GitHub, GitLab or Jira issue |
|
|
516
549
|
| `loki quick "<task>"` | Small task, lean path, no PR |
|
|
517
550
|
| `loki backlog owner/repo --all\|--label X\|--issues N,N` | Run every matching open issue, N in parallel (`--concurrency N`, `--dry-run`) |
|
|
551
|
+
| `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
552
|
| `loki status [--json]` | Current status |
|
|
519
553
|
| `loki verify [run-id]` | Re-check a sealed receipt (exit codes above) |
|
|
520
554
|
| `loki doctor [--json] [--airgap]` | Check environment and providers |
|
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.8
|
|
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.8 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
10.6.
|
|
1
|
+
10.6.8
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""loki workspace: run one issue across several repos (D51 Phase B, D65).
|
|
3
|
+
|
|
4
|
+
loki workspace list
|
|
5
|
+
loki workspace run <name> <issue-ref>
|
|
6
|
+
|
|
7
|
+
Gated by LOKI_WORKSPACES=1. Workspaces are defined in loki.yaml:
|
|
8
|
+
|
|
9
|
+
workspaces:
|
|
10
|
+
shop:
|
|
11
|
+
repos:
|
|
12
|
+
- {repo: acme/api, path: ~/src/api, setup: "npm ci"}
|
|
13
|
+
- {repo: acme/web, path: ~/src/web, after: [acme/api]}
|
|
14
|
+
integration: {command: "make e2e", timeout_s: 900}
|
|
15
|
+
|
|
16
|
+
Each repo gets its own worktree and engine run (continue-and-report: a failing
|
|
17
|
+
repo does not stop the others; dependents of a failed repo are SKIPPED). After
|
|
18
|
+
every repo has finished, the integration command runs once with each run branch
|
|
19
|
+
checked out, and evidence lands in
|
|
20
|
+
.loki/workspaces/<name>/<run-id>/integration.json. Integration evidence is not
|
|
21
|
+
part of the Seal.
|
|
22
|
+
|
|
23
|
+
Exit: 0 all repos ok and integration not failed; 1 otherwise; 2 usage/config.
|
|
24
|
+
|
|
25
|
+
Test seam: LOKI_WORKSPACE_LAUNCHER replaces the engine launcher (called with
|
|
26
|
+
the issue ref or task text, cwd = the repo worktree).
|
|
27
|
+
"""
|
|
28
|
+
import datetime
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import re
|
|
33
|
+
import signal
|
|
34
|
+
import subprocess
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
sys.path.insert(0, HERE)
|
|
39
|
+
|
|
40
|
+
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
|
41
|
+
REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
42
|
+
REF_RE = re.compile(r"^([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)#(\d+)$")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def say(msg):
|
|
46
|
+
print(msg, flush=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def enabled(env=None):
|
|
50
|
+
return (os.environ if env is None else env).get("LOKI_WORKSPACES") == "1"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def git(*args, cwd=None):
|
|
54
|
+
return subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_workspaces(cfg=None):
|
|
58
|
+
"""Return (workspaces, errors) from loki.yaml (or the given cfg mapping)."""
|
|
59
|
+
if cfg is None:
|
|
60
|
+
import loki_yaml
|
|
61
|
+
cfg, _path, errors = loki_yaml.load()
|
|
62
|
+
if errors:
|
|
63
|
+
return {}, errors
|
|
64
|
+
return cfg.get("workspaces") or {}, []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def order_repos(ws):
|
|
68
|
+
"""Return repo entries ordered so every `after` predecessor comes first.
|
|
69
|
+
|
|
70
|
+
Raises ValueError on an unknown `after` entry, a duplicate repo or a cycle.
|
|
71
|
+
"""
|
|
72
|
+
entries = list(ws.get("repos") or [])
|
|
73
|
+
names = [e.get("repo") for e in entries]
|
|
74
|
+
if len(set(names)) != len(names):
|
|
75
|
+
raise ValueError("duplicate repo in workspace")
|
|
76
|
+
for e in entries:
|
|
77
|
+
if not isinstance(e.get("repo"), str) or not REPO_RE.match(e["repo"]):
|
|
78
|
+
raise ValueError("repo must be owner/name, got %r" % (e.get("repo"),))
|
|
79
|
+
for dep in e.get("after") or []:
|
|
80
|
+
if dep not in names:
|
|
81
|
+
raise ValueError("%s: unknown after repo %s" % (e["repo"], dep))
|
|
82
|
+
done, ordered, remaining = set(), [], entries
|
|
83
|
+
while remaining:
|
|
84
|
+
ready = [e for e in remaining if all(d in done for d in e.get("after") or [])]
|
|
85
|
+
if not ready:
|
|
86
|
+
raise ValueError("cycle in `after`: " + ", ".join(e["repo"] for e in remaining))
|
|
87
|
+
for e in ready:
|
|
88
|
+
ordered.append(e)
|
|
89
|
+
done.add(e["repo"])
|
|
90
|
+
remaining = [e for e in remaining if e not in ready]
|
|
91
|
+
return ordered
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _slug(repo):
|
|
95
|
+
return repo.replace("/", "__")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def source_checkout(entry):
|
|
99
|
+
"""Local checkout for a repo entry: its `path`, else clone into ~/.loki/repos/<slug>."""
|
|
100
|
+
if entry.get("path"):
|
|
101
|
+
return os.path.abspath(os.path.expanduser(entry["path"]))
|
|
102
|
+
dest = os.path.join(os.path.expanduser("~"), ".loki", "repos", _slug(entry["repo"]))
|
|
103
|
+
if not os.path.isdir(os.path.join(dest, ".git")):
|
|
104
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
105
|
+
r = subprocess.run(["git", "clone", "https://github.com/%s.git" % entry["repo"], dest],
|
|
106
|
+
capture_output=True, text=True)
|
|
107
|
+
if r.returncode != 0:
|
|
108
|
+
raise RuntimeError("clone failed: " + (r.stderr.strip().splitlines() or ["git error"])[-1])
|
|
109
|
+
return dest
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def prepare(source, dest, branch, setup):
|
|
113
|
+
"""Make a worktree via worktree_prep when available, else a minimal `git worktree add`."""
|
|
114
|
+
try:
|
|
115
|
+
import worktree_prep
|
|
116
|
+
res = worktree_prep.prepare_worktree(source, dest, branch, setup=setup)
|
|
117
|
+
return res["path"]
|
|
118
|
+
except ImportError:
|
|
119
|
+
pass
|
|
120
|
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
|
121
|
+
r = git("worktree", "add", "-B", branch, dest, "HEAD", cwd=source)
|
|
122
|
+
if r.returncode != 0:
|
|
123
|
+
raise RuntimeError("worktree: " + (r.stderr.strip().splitlines() or ["git error"])[-1])
|
|
124
|
+
if setup:
|
|
125
|
+
s = subprocess.run(setup, shell=True, cwd=dest, capture_output=True, text=True)
|
|
126
|
+
if s.returncode != 0:
|
|
127
|
+
raise RuntimeError("setup failed: " + (s.stderr.strip().splitlines() or ["no output"])[-1])
|
|
128
|
+
return dest
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def task_for(entry, ref, siblings):
|
|
132
|
+
"""(launcher argument, extra env) for a repo: the issue ref when it lives here, else task text."""
|
|
133
|
+
m = REF_RE.match(ref)
|
|
134
|
+
if m and m.group(1) == entry["repo"]:
|
|
135
|
+
return ref, {}
|
|
136
|
+
text = ("Implement the change described by %s for the repo %s. Sibling worktrees (read-only "
|
|
137
|
+
"context, do not edit): %s" % (ref, entry["repo"], ", ".join(siblings) or "none"))
|
|
138
|
+
return text, {"LOKI_E10_TASK_TEXT": text}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_integration(ws, run_dir, worktrees, heads):
|
|
142
|
+
"""Run the integration command once; return the evidence dict."""
|
|
143
|
+
integ = ws.get("integration") or {}
|
|
144
|
+
cmd = integ.get("command")
|
|
145
|
+
ev = {"heads": heads, "exit_code": None, "log_sha256": None, "status": "not_configured",
|
|
146
|
+
"seal": False, "note": "integration evidence is not part of the Seal"}
|
|
147
|
+
if not cmd:
|
|
148
|
+
return ev
|
|
149
|
+
timeout = integ.get("timeout_s", 900)
|
|
150
|
+
env = dict(os.environ)
|
|
151
|
+
for repo, wt in worktrees.items():
|
|
152
|
+
env["LOKI_WS_DIR_" + re.sub(r"[^A-Za-z0-9]", "_", repo).upper()] = wt
|
|
153
|
+
log_path = os.path.join(run_dir, "integration.log")
|
|
154
|
+
with open(log_path, "wb") as lf:
|
|
155
|
+
p = subprocess.Popen(cmd, shell=True, cwd=run_dir, env=env, stdout=lf, stderr=subprocess.STDOUT,
|
|
156
|
+
stdin=subprocess.DEVNULL, start_new_session=True)
|
|
157
|
+
try:
|
|
158
|
+
rc = p.wait(timeout=timeout)
|
|
159
|
+
ev["status"] = "passed" if rc == 0 else "failed"
|
|
160
|
+
except subprocess.TimeoutExpired:
|
|
161
|
+
try:
|
|
162
|
+
os.killpg(p.pid, signal.SIGKILL)
|
|
163
|
+
except OSError:
|
|
164
|
+
pass
|
|
165
|
+
p.wait()
|
|
166
|
+
rc = 124
|
|
167
|
+
ev["status"] = "failed"
|
|
168
|
+
ev["note"] += "; timeout after %ss" % timeout
|
|
169
|
+
ev["exit_code"] = rc
|
|
170
|
+
with open(log_path, "rb") as f:
|
|
171
|
+
ev["log_sha256"] = hashlib.sha256(f.read()).hexdigest()
|
|
172
|
+
return ev
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def run_workspace(name, ref, workspaces, base_dir=None, launcher=None):
|
|
176
|
+
"""Run the workspace; return (exit_code, evidence_path_or_None)."""
|
|
177
|
+
ws = workspaces.get(name)
|
|
178
|
+
if ws is None:
|
|
179
|
+
say("workspace: unknown workspace %r (known: %s)" % (name, ", ".join(sorted(workspaces)) or "none"))
|
|
180
|
+
return 2, None
|
|
181
|
+
try:
|
|
182
|
+
ordered = order_repos(ws)
|
|
183
|
+
except ValueError as e:
|
|
184
|
+
say("workspace: %s: %s" % (name, e))
|
|
185
|
+
return 2, None
|
|
186
|
+
if not ordered:
|
|
187
|
+
say("workspace: %s has no repos" % name)
|
|
188
|
+
return 2, None
|
|
189
|
+
|
|
190
|
+
base_dir = base_dir or os.getcwd()
|
|
191
|
+
run_id = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
|
|
192
|
+
run_dir = os.path.join(base_dir, ".loki", "workspaces", name, run_id)
|
|
193
|
+
os.makedirs(run_dir, exist_ok=True)
|
|
194
|
+
launcher = launcher or os.environ.get("LOKI_WORKSPACE_LAUNCHER") or os.path.join(REPO_ROOT, "bin", "loki")
|
|
195
|
+
child_env_base = dict(os.environ, LOKI_ENGINE="v10", LOKI_NO_BROWSER="1")
|
|
196
|
+
|
|
197
|
+
outcome, worktrees, heads = {}, {}, {}
|
|
198
|
+
running = {}
|
|
199
|
+
|
|
200
|
+
def stop(*_):
|
|
201
|
+
for p in running.values():
|
|
202
|
+
try:
|
|
203
|
+
os.killpg(p.pid, signal.SIGTERM)
|
|
204
|
+
except OSError:
|
|
205
|
+
pass
|
|
206
|
+
sys.exit(130)
|
|
207
|
+
signal.signal(signal.SIGTERM, stop)
|
|
208
|
+
|
|
209
|
+
for i, entry in enumerate(ordered):
|
|
210
|
+
repo = entry["repo"]
|
|
211
|
+
failed_dep = [d for d in entry.get("after") or [] if outcome.get(d) != "ok"]
|
|
212
|
+
if failed_dep:
|
|
213
|
+
outcome[repo] = "SKIPPED (predecessor failed: %s)" % ", ".join(failed_dep)
|
|
214
|
+
say("workspace: %s %s" % (repo, outcome[repo]))
|
|
215
|
+
continue
|
|
216
|
+
try:
|
|
217
|
+
src = source_checkout(entry)
|
|
218
|
+
dest = os.path.join(run_dir, "worktrees", _slug(repo))
|
|
219
|
+
wt = prepare(src, dest, "loki/ws-%s-%d" % (run_id, i), entry.get("setup"))
|
|
220
|
+
except (RuntimeError, OSError) as e:
|
|
221
|
+
outcome[repo] = "FAILED: %s" % e
|
|
222
|
+
say("workspace: %s %s" % (repo, outcome[repo]))
|
|
223
|
+
continue
|
|
224
|
+
worktrees[repo] = wt
|
|
225
|
+
siblings = [os.path.join(run_dir, "worktrees", _slug(o["repo"])) for o in ordered if o["repo"] != repo]
|
|
226
|
+
arg, extra = task_for(entry, ref, siblings)
|
|
227
|
+
log = os.path.join(run_dir, "%s.log" % _slug(repo))
|
|
228
|
+
say("workspace: %s running" % repo)
|
|
229
|
+
with open(log, "w") as lf:
|
|
230
|
+
p = subprocess.Popen([launcher, arg], cwd=wt, env=dict(child_env_base, **extra), stdout=lf,
|
|
231
|
+
stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, start_new_session=True)
|
|
232
|
+
running[repo] = p
|
|
233
|
+
rc = p.wait()
|
|
234
|
+
running.pop(repo, None)
|
|
235
|
+
outcome[repo] = "ok" if rc == 0 else "FAILED: exit %d" % rc
|
|
236
|
+
say("workspace: %s %s" % (repo, outcome[repo]))
|
|
237
|
+
|
|
238
|
+
for repo, wt in worktrees.items():
|
|
239
|
+
r = git("rev-parse", "HEAD", cwd=wt)
|
|
240
|
+
heads[repo] = r.stdout.strip() if r.returncode == 0 else None
|
|
241
|
+
|
|
242
|
+
ev = run_integration(ws, run_dir, worktrees, heads)
|
|
243
|
+
ev["outcomes"] = outcome
|
|
244
|
+
ev_path = os.path.join(run_dir, "integration.json")
|
|
245
|
+
tmp = ev_path + ".tmp"
|
|
246
|
+
with open(tmp, "w") as f:
|
|
247
|
+
json.dump(ev, f, indent=2, sort_keys=True)
|
|
248
|
+
os.replace(tmp, ev_path)
|
|
249
|
+
|
|
250
|
+
say("")
|
|
251
|
+
say("Summary: workspace %s (%s)" % (name, run_id))
|
|
252
|
+
for repo, res in outcome.items():
|
|
253
|
+
say("%-30s %s" % (repo, res))
|
|
254
|
+
say("%-30s %s" % ("integration", ev["status"]))
|
|
255
|
+
bad = any(v != "ok" for v in outcome.values()) or ev["status"] == "failed"
|
|
256
|
+
return (1 if bad else 0), ev_path
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main(argv):
|
|
260
|
+
if not enabled():
|
|
261
|
+
say("loki workspace is disabled. Set LOKI_WORKSPACES=1 to use it.")
|
|
262
|
+
return 2
|
|
263
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
264
|
+
say("Usage: loki workspace list | run <name> <owner/repo#N | task text>")
|
|
265
|
+
return 0 if argv else 2
|
|
266
|
+
try:
|
|
267
|
+
workspaces, errors = load_workspaces()
|
|
268
|
+
except Exception as e: # CannotCheck or parse failure
|
|
269
|
+
say("workspace: cannot read loki.yaml: %s" % e)
|
|
270
|
+
return 2
|
|
271
|
+
if errors:
|
|
272
|
+
for e in errors:
|
|
273
|
+
say("workspace: config error: %s" % e)
|
|
274
|
+
return 2
|
|
275
|
+
cmd = argv[0]
|
|
276
|
+
if cmd == "list":
|
|
277
|
+
if not workspaces:
|
|
278
|
+
say("no workspaces defined in loki.yaml")
|
|
279
|
+
for n, ws in sorted(workspaces.items()):
|
|
280
|
+
say("%s: %s" % (n, ", ".join(e.get("repo", "?") for e in ws.get("repos") or [])))
|
|
281
|
+
return 0
|
|
282
|
+
if cmd == "run" and len(argv) >= 3:
|
|
283
|
+
rc, _ = run_workspace(argv[1], " ".join(argv[2:]), workspaces)
|
|
284
|
+
return rc
|
|
285
|
+
say("Usage: loki workspace list | run <name> <issue-ref>")
|
|
286
|
+
return 2
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
sys.exit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared worktree substrate (D51 Phase B, D61 slice 7).
|
|
3
|
+
|
|
4
|
+
prepare_worktree() creates a git worktree for a run from a clean start point,
|
|
5
|
+
never carrying the source checkout's uncommitted edits, and prepares
|
|
6
|
+
dependencies (setup command, else copy-on-write copy of ignored dep dirs).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import fcntl
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
DEP_DIRS = ("node_modules", ".venv", "venv", "vendor")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _git(repo, *args, check=True):
|
|
20
|
+
return subprocess.run(
|
|
21
|
+
["git", "-C", repo, *args], capture_output=True, text=True, check=check
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _slug(repo):
|
|
26
|
+
real = os.path.realpath(repo)
|
|
27
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "-", real).strip("-")[-120:] or "repo"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _lock_path(repo):
|
|
31
|
+
d = os.path.join(os.path.expanduser("~"), ".loki", "repos")
|
|
32
|
+
os.makedirs(d, exist_ok=True)
|
|
33
|
+
return os.path.join(d, _slug(repo) + ".lock")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _start_point(repo, base):
|
|
37
|
+
if base:
|
|
38
|
+
return base
|
|
39
|
+
if _git(repo, "remote", check=False).stdout.strip():
|
|
40
|
+
_git(repo, "fetch", "--quiet", check=False)
|
|
41
|
+
for ref in ("origin/HEAD", "HEAD"):
|
|
42
|
+
probe = _git(repo, "rev-parse", "--verify", "--quiet", ref + "^{commit}", check=False)
|
|
43
|
+
if probe.returncode == 0:
|
|
44
|
+
return ref
|
|
45
|
+
raise RuntimeError("no start point: repository has no commits")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _left_out_changes(repo):
|
|
49
|
+
out = _git(repo, "status", "--porcelain", check=False).stdout
|
|
50
|
+
return len([line for line in out.splitlines() if line.strip()])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cow_copy(src, dst):
|
|
54
|
+
if sys.platform == "darwin":
|
|
55
|
+
cmds = [["cp", "-c", "-R", src, dst]]
|
|
56
|
+
else:
|
|
57
|
+
cmds = [["cp", "-R", "--reflink=auto", src, dst]]
|
|
58
|
+
cmds.append(["cp", "-R", src, dst])
|
|
59
|
+
for cmd in cmds:
|
|
60
|
+
if os.path.lexists(dst):
|
|
61
|
+
subprocess.run(["rm", "-rf", dst], check=False)
|
|
62
|
+
if subprocess.run(cmd, capture_output=True).returncode == 0:
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _escapes_source(path):
|
|
68
|
+
"""True if a dep dir has an absolute symlink or an editable finder."""
|
|
69
|
+
for root, dirs, files in os.walk(path):
|
|
70
|
+
for name in dirs + files:
|
|
71
|
+
p = os.path.join(root, name)
|
|
72
|
+
if os.path.islink(p):
|
|
73
|
+
if os.path.isabs(os.readlink(p)):
|
|
74
|
+
return True
|
|
75
|
+
elif name.endswith(".pth") or name.startswith("__editable__"):
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _copy_deps(source, dest, start):
|
|
81
|
+
copied = 0
|
|
82
|
+
for name in DEP_DIRS:
|
|
83
|
+
s = os.path.join(source, name)
|
|
84
|
+
if not os.path.isdir(s) or os.path.islink(s):
|
|
85
|
+
continue
|
|
86
|
+
if _git(source, "check-ignore", "-q", name + "/", check=False).returncode != 0:
|
|
87
|
+
continue
|
|
88
|
+
if _git(source, "ls-tree", "-d", start, name, check=False).stdout.strip():
|
|
89
|
+
continue
|
|
90
|
+
if _escapes_source(s):
|
|
91
|
+
raise RuntimeError("deps: copy unsafe, declare setup")
|
|
92
|
+
if not _cow_copy(s, os.path.join(dest, name)):
|
|
93
|
+
raise RuntimeError("deps: copy failed, declare setup")
|
|
94
|
+
copied += 1
|
|
95
|
+
return copied
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def prepare_worktree(source_repo, dest, branch, setup=None, base=None):
|
|
99
|
+
source_repo = os.path.abspath(source_repo)
|
|
100
|
+
dest = os.path.abspath(dest)
|
|
101
|
+
lock = open(_lock_path(source_repo), "w")
|
|
102
|
+
try:
|
|
103
|
+
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
104
|
+
left_out = _left_out_changes(source_repo)
|
|
105
|
+
start = _start_point(source_repo, base)
|
|
106
|
+
base_sha = _git(source_repo, "rev-parse", start + "^{commit}").stdout.strip()
|
|
107
|
+
_git(source_repo, "worktree", "add", "-b", branch, dest, base_sha)
|
|
108
|
+
finally:
|
|
109
|
+
fcntl.flock(lock, fcntl.LOCK_UN)
|
|
110
|
+
lock.close()
|
|
111
|
+
|
|
112
|
+
deps = "none"
|
|
113
|
+
if setup:
|
|
114
|
+
r = subprocess.run(setup, shell=True, cwd=dest)
|
|
115
|
+
if r.returncode != 0:
|
|
116
|
+
raise RuntimeError("deps: setup command failed (exit %d)" % r.returncode)
|
|
117
|
+
deps = "setup"
|
|
118
|
+
elif _copy_deps(source_repo, dest, base_sha):
|
|
119
|
+
deps = "cow" if sys.platform in ("darwin", "linux") else "copy"
|
|
120
|
+
return {"path": dest, "base_sha": base_sha, "deps": deps, "left_out_changes": left_out}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main(argv=None):
|
|
124
|
+
import argparse
|
|
125
|
+
|
|
126
|
+
p = argparse.ArgumentParser(description="Prepare an isolated git worktree")
|
|
127
|
+
p.add_argument("source")
|
|
128
|
+
p.add_argument("dest")
|
|
129
|
+
p.add_argument("branch")
|
|
130
|
+
p.add_argument("--setup", default=None)
|
|
131
|
+
p.add_argument("--base", default=None)
|
|
132
|
+
a = p.parse_args(argv)
|
|
133
|
+
try:
|
|
134
|
+
res = prepare_worktree(a.source, a.dest, a.branch, a.setup, a.base)
|
|
135
|
+
except (RuntimeError, subprocess.CalledProcessError) as e:
|
|
136
|
+
print(json.dumps({"error": str(e)}))
|
|
137
|
+
return 1
|
|
138
|
+
print(json.dumps(res))
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
sys.exit(main())
|
package/autonomy/loki
CHANGED
|
@@ -1630,6 +1630,7 @@ show_help() {
|
|
|
1630
1630
|
echo " start [SPEC] Start a build (PRD file, GitHub/GitLab/Jira/Azure issue, or no arg)"
|
|
1631
1631
|
echo " plan <PRD-file> Dry-run analysis: complexity, cost, execution plan"
|
|
1632
1632
|
echo " backlog <owner/repo> --all|--label X|--issues N,N Run every matching open issue, N in parallel"
|
|
1633
|
+
echo " workspace list | run <name> <ref> Multi-repo runs from loki.yaml workspaces (LOKI_WORKSPACES=1)"
|
|
1633
1634
|
echo " quickstart [idea] Guided first build: setup, idea, template, plan, go"
|
|
1634
1635
|
echo ""
|
|
1635
1636
|
echo "Session:"
|
|
@@ -7324,6 +7325,17 @@ _loki_dashboard_bind_is_safe() {
|
|
|
7324
7325
|
cmd_dashboard() {
|
|
7325
7326
|
local subcommand="${1:-}"
|
|
7326
7327
|
|
|
7328
|
+
# LOKI_CONTROL_DEFAULT=1 (off by default): open the Control Plane instead of the old dashboard.
|
|
7329
|
+
# The old dashboard stays available when the flag is unset, and via `loki dashboard stop|status|url`.
|
|
7330
|
+
if [ "${LOKI_CONTROL_DEFAULT:-0}" = "1" ]; then
|
|
7331
|
+
case "$subcommand" in
|
|
7332
|
+
""|start|open)
|
|
7333
|
+
echo "LOKI_CONTROL_DEFAULT=1: starting the Control Plane (unset it to use the old dashboard)"
|
|
7334
|
+
LOKI_CONTROL=1 exec "$0" control serve
|
|
7335
|
+
;;
|
|
7336
|
+
esac
|
|
7337
|
+
fi
|
|
7338
|
+
|
|
7327
7339
|
# If no subcommand provided, show help (backward compatible with old behavior)
|
|
7328
7340
|
if [ -z "$subcommand" ]; then
|
|
7329
7341
|
cmd_dashboard_help
|
|
@@ -12909,6 +12921,16 @@ cmd_config_schema() {
|
|
|
12909
12921
|
loki_config_generate_schema
|
|
12910
12922
|
}
|
|
12911
12923
|
|
|
12924
|
+
# D65: multi-repo workspaces (D51 Phase B). Gated by LOKI_WORKSPACES=1.
|
|
12925
|
+
cmd_workspace() {
|
|
12926
|
+
if [ "${LOKI_WORKSPACES:-}" != "1" ]; then
|
|
12927
|
+
echo -e "${RED}loki workspace is disabled.${NC} Set LOKI_WORKSPACES=1 to use it." >&2
|
|
12928
|
+
return 2
|
|
12929
|
+
fi
|
|
12930
|
+
command -v python3 >/dev/null 2>&1 || { echo -e "${RED}loki workspace: python3 is required${NC}" >&2; return 2; }
|
|
12931
|
+
python3 "$_LOKI_SCRIPT_DIR/lib/workspace.py" "$@"
|
|
12932
|
+
}
|
|
12933
|
+
|
|
12912
12934
|
# D51: headless backlog runner. One v10 issue-mode run per open issue, each in
|
|
12913
12935
|
# its own worktree and branch, at most N in parallel (loki.yaml concurrency).
|
|
12914
12936
|
cmd_backlog() {
|
|
@@ -21922,6 +21944,9 @@ main() {
|
|
|
21922
21944
|
backlog)
|
|
21923
21945
|
cmd_backlog "$@"
|
|
21924
21946
|
;;
|
|
21947
|
+
workspace)
|
|
21948
|
+
cmd_workspace "$@"
|
|
21949
|
+
;;
|
|
21925
21950
|
config)
|
|
21926
21951
|
cmd_config "$@"
|
|
21927
21952
|
;;
|
package/bin/loki
CHANGED
|
@@ -567,7 +567,7 @@ fi
|
|
|
567
567
|
# Two-token routes (provider show/list, memory list/index) match on the first
|
|
568
568
|
# token only; the Bun dispatcher handles subcommand routing internally.
|
|
569
569
|
case "${1:-}" in
|
|
570
|
-
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash|control)
|
|
570
|
+
version|--version|-v|status|stats|doctor|provider|memory|rollback|internal|kpis|trust|proof|receipt|wiki|crash|control|contract)
|
|
571
571
|
# v7.5.2: rollback added (wires loki-ts/src/commands/rollback.ts).
|
|
572
572
|
# v7.5.3: internal added for autonomy/run.sh phase1-hooks calls.
|
|
573
573
|
# v7.5.28: kpis added (Phase K MVP: read-only KPI snapshot).
|
package/completions/_loki
CHANGED
|
@@ -207,6 +207,7 @@ function _loki_commands {
|
|
|
207
207
|
'otel:OpenTelemetry management'
|
|
208
208
|
'wiki:Wiki commands'
|
|
209
209
|
'control:Control plane (preview, LOKI_CONTROL=1)'
|
|
210
|
+
'workspace:Multi-project workspaces (preview, LOKI_WORKSPACES=1)'
|
|
210
211
|
'backlog:Run every matching open issue, N in parallel'
|
|
211
212
|
'explain:Explain Loki concepts'
|
|
212
213
|
'optimize:Optimization commands'
|
package/completions/loki.bash
CHANGED
|
@@ -5,8 +5,7 @@ _loki_completion() {
|
|
|
5
5
|
_init_completion || return
|
|
6
6
|
|
|
7
7
|
# Main subcommands (must match autonomy/loki main case statement)
|
|
8
|
-
local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys"
|
|
9
|
-
local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys"
|
|
8
|
+
local main_commands="start backlog quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash control workspace open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help keys"
|
|
10
9
|
|
|
11
10
|
# 1. If we are on the first argument (subcommand)
|
|
12
11
|
if [[ $cword -eq 1 ]]; then
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
|
|
4
4
|
|
|
5
|
-
**Version:** v10.6.
|
|
5
|
+
**Version:** v10.6.8
|
|
6
6
|
|
|
7
7
|
**Engine note:** `loki "<task>"`, `loki owner/repo#N` and `loki quick` run the Loki 10 engine. `loki start` still routes to the legacy engine, which is being removed (planned work resumes 2026-10-07); prefer `loki owner/repo#N`. See [docs/v10/GUIDE.md](v10/GUIDE.md).
|
|
8
8
|
|
|
@@ -53,7 +53,7 @@ Measured on this machine, not asserted.
|
|
|
53
53
|
| **1 Systems Thinking** | STRONG | 8 quality gates, RARV-C loop, council, Evidence Receipt, dual-route parity enforced by test |
|
|
54
54
|
| **2 Speed** | MEASURED, UNOPTIMISED | agent call = **980s = 96%** of iteration; all gates together = 44s |
|
|
55
55
|
| **3 Reliability** | STRONG, newly so | 73 mutation-proven trust invariants; four gates were shipping broken until v8.38.0 |
|
|
56
|
-
| **4 Extensibility** | STRONG | 5 providers, agent type registry, MCP (
|
|
56
|
+
| **4 Extensibility** | STRONG | 5 providers, agent type registry, MCP (39 tools), plugin marketplace |
|
|
57
57
|
| **5 Feedback Loops / Evals** | **BROKEN** | `cost_usd == 0` on **3 of 5** efficiency records; no eval score for our own harness |
|
|
58
58
|
|
|
59
59
|
**Principle 5 is the gap, and it is the one Wang weights highest.** He states the
|