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.
Files changed (100) hide show
  1. package/README.md +35 -1
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/lib/workspace.py +290 -0
  5. package/autonomy/lib/worktree_prep.py +143 -0
  6. package/autonomy/loki +25 -0
  7. package/bin/loki +1 -1
  8. package/completions/_loki +1 -0
  9. package/completions/loki.bash +1 -2
  10. package/dashboard/__init__.py +1 -1
  11. package/docs/INSTALLATION.md +1 -1
  12. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  13. package/docs/WORKSPACES.md +29 -0
  14. package/docs/alternative-installations.md +3 -1
  15. package/docs/control-plane-container.md +45 -0
  16. package/docs/dev/architecture-reference.md +4 -0
  17. package/docs/dev/project-structure.md +1 -1
  18. package/docs/slack.md +26 -0
  19. package/docs/v10/BOARD.md +26 -1
  20. package/docs/v10/CONTROL-PLANE.md +4 -0
  21. package/docs/v10/D61-SPEED.md +7 -2
  22. package/docs/v10/DECISIONS.md +24 -0
  23. package/docs/v10/DEPS.md +6 -2
  24. package/docs/v10/E2E-D65.md +87 -0
  25. package/docs/v10/HANDOFF-NEXT-WEEK.md +10 -4
  26. package/docs/v10/PROGRESS.md +13 -0
  27. package/docs/v10/ROADMAP-D63.md +35 -0
  28. package/loki-ts/dist/loki.js +528 -491
  29. package/mcp/__init__.py +1 -1
  30. package/mcp/server.py +76 -0
  31. package/mcp/tests/test_v10_tools.py +137 -0
  32. package/mcp/v10_tools.py +172 -0
  33. package/package.json +1 -1
  34. package/packages/control-plane/dist/server.js +56 -4
  35. package/packages/control-plane/src/server/answer.ts +33 -0
  36. package/packages/control-plane/src/server/app.ts +17 -1
  37. package/packages/control-plane/src/server/runs.ts +2 -0
  38. package/packages/control-plane/src/server/serve.ts +2 -2
  39. package/packages/control-plane/ui/dist/assets/index-53E32yrO.js +114 -0
  40. package/packages/control-plane/ui/dist/assets/{index-BH3LtKcG.css → index-CFfrqCx2.css} +1 -1
  41. package/packages/control-plane/ui/dist/index.html +2 -2
  42. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  43. package/web-app/dist/assets/{AdminPage-DyjnUcaP.js → AdminPage-DN_pe8vo.js} +1 -1
  44. package/web-app/dist/assets/{Avatar-CY0sQNao.js → Avatar-BnQsr8z-.js} +1 -1
  45. package/web-app/dist/assets/{Badge-BmwTRu72.js → Badge-DzERiZn2.js} +1 -1
  46. package/web-app/dist/assets/{Button-BRf5aTlb.js → Button-CaaoxQAt.js} +1 -1
  47. package/web-app/dist/assets/{CockpitPage-RR94HuxS.js → CockpitPage-SXhnysJA.js} +1 -1
  48. package/web-app/dist/assets/{ComparePage-CFWAuPRO.js → ComparePage-krxTCFJY.js} +1 -1
  49. package/web-app/dist/assets/{ErrorBoundary-4cDADrG-.js → ErrorBoundary-BL96ESMc.js} +1 -1
  50. package/web-app/dist/assets/{EvidenceReceiptPanel-ChPFx2eM.js → EvidenceReceiptPanel-PUiGSpkB.js} +1 -1
  51. package/web-app/dist/assets/{GitHubIssuesPanel-B0TAjyeW.js → GitHubIssuesPanel-C0hPs-P2.js} +1 -1
  52. package/web-app/dist/assets/{GitHubPRsPanel-BbszEwW9.js → GitHubPRsPanel-CLvqNUlu.js} +1 -1
  53. package/web-app/dist/assets/{HomePage-DrBgzs1d.js → HomePage-X-sq_Rc9.js} +2 -2
  54. package/web-app/dist/assets/{LoginPage-Bu7b2DGz.js → LoginPage-DhQuwdua.js} +1 -1
  55. package/web-app/dist/assets/{MagicPage-BFEYp631.js → MagicPage-CP2ODjfn.js} +1 -1
  56. package/web-app/dist/assets/{MetricsPage-eIe2naVR.js → MetricsPage-C6SYiQES.js} +1 -1
  57. package/web-app/dist/assets/{NotFoundPage-BVWYkNt9.js → NotFoundPage-CCDxxYc-.js} +1 -1
  58. package/web-app/dist/assets/{ProjectPage-zmxyu732.js → ProjectPage-yDF--8FQ.js} +3 -3
  59. package/web-app/dist/assets/{ProjectsPage-Dq5NdCak.js → ProjectsPage-BBLspdLn.js} +1 -1
  60. package/web-app/dist/assets/{SettingsPage-CCrJOMe0.js → SettingsPage-C_sWbQRw.js} +1 -1
  61. package/web-app/dist/assets/{ShowcasePage-n-Ipzp01.js → ShowcasePage-xCMuL0N8.js} +1 -1
  62. package/web-app/dist/assets/{SystemSettingsPage-CP_rjYXK.js → SystemSettingsPage-DnJKGCx7.js} +1 -1
  63. package/web-app/dist/assets/{TeamsPage-B_P9DepW.js → TeamsPage-BBXdhrSs.js} +1 -1
  64. package/web-app/dist/assets/{TemplatesPage-h-3A6PQT.js → TemplatesPage-AZ1Lr1rn.js} +1 -1
  65. package/web-app/dist/assets/{TerminalOutput-pD0nJ1BU.js → TerminalOutput-CtIm9tXc.js} +1 -1
  66. package/web-app/dist/assets/{activity-ChFwzT-d.js → activity-Dk1FK25Q.js} +1 -1
  67. package/web-app/dist/assets/{bell-BX-eK8Gu.js → bell-dhqXyzP4.js} +1 -1
  68. package/web-app/dist/assets/{bot-fj5PkPtR.js → bot-CExHd4Kf.js} +1 -1
  69. package/web-app/dist/assets/{check-BGAbHlvD.js → check-D51zaA1a.js} +1 -1
  70. package/web-app/dist/assets/{chevron-left-BzFUxHgb.js → chevron-left-DEwnQzH1.js} +1 -1
  71. package/web-app/dist/assets/{circle-alert-BQrg5DWl.js → circle-alert-C6U90Msk.js} +1 -1
  72. package/web-app/dist/assets/{clock-CTu9lkVo.js → clock-6mFbMzzH.js} +1 -1
  73. package/web-app/dist/assets/{cloud-DrIENRiE.js → cloud-BrZZtE7F.js} +1 -1
  74. package/web-app/dist/assets/{code-xml-PaoVpfur.js → code-xml-DTFzeepP.js} +1 -1
  75. package/web-app/dist/assets/{database-DMmkAotn.js → database-B5rEiTre.js} +1 -1
  76. package/web-app/dist/assets/{dollar-sign-B2LQdBoX.js → dollar-sign-bQs4rlS_.js} +1 -1
  77. package/web-app/dist/assets/{file-code-corner-Dfv1XXeR.js → file-code-corner-Mo6e6qbf.js} +1 -1
  78. package/web-app/dist/assets/{file-plus-D2Wzf2wd.js → file-plus-DzHZ0jpP.js} +1 -1
  79. package/web-app/dist/assets/{globe-DxotVWt8.js → globe-DYN9UhTa.js} +1 -1
  80. package/web-app/dist/assets/{hammer-Brfy5ZrJ.js → hammer-bB0hSRcJ.js} +1 -1
  81. package/web-app/dist/assets/{index-BfhHMwi8.js → index-CIH5CqZB.js} +3 -3
  82. package/web-app/dist/assets/{layers-DfxSWDvp.js → layers-qhFkY8Nu.js} +1 -1
  83. package/web-app/dist/assets/{loader-circle-CxIrzNwx.js → loader-circle-BGBkaYH7.js} +1 -1
  84. package/web-app/dist/assets/{lock-m6nzhtpU.js → lock-DIcsNFum.js} +1 -1
  85. package/web-app/dist/assets/{package-ri9oE9Jc.js → package-BdIIbZhS.js} +1 -1
  86. package/web-app/dist/assets/{plus-DWeTAMVV.js → plus-Co1Bv8zT.js} +1 -1
  87. package/web-app/dist/assets/{refresh-cw-BGMSpaMh.js → refresh-cw-CGF4ABWH.js} +1 -1
  88. package/web-app/dist/assets/{rotate-ccw-DD1xBD_b.js → rotate-ccw-BX10Q79h.js} +1 -1
  89. package/web-app/dist/assets/{scroll-text-BCCR_AN9.js → scroll-text-CbXDlpGp.js} +1 -1
  90. package/web-app/dist/assets/{server-CmMWPIjS.js → server-CIhi6yUO.js} +1 -1
  91. package/web-app/dist/assets/{shield-alert-BZZmBVEJ.js → shield-alert-DJ5fB6Dg.js} +1 -1
  92. package/web-app/dist/assets/{thumbs-up-OaxbNRXu.js → thumbs-up-BEft2T94.js} +1 -1
  93. package/web-app/dist/assets/{trash-2-DmcSzTfr.js → trash-2-CWGUPn7Y.js} +1 -1
  94. package/web-app/dist/assets/{trending-down-BmfXLykz.js → trending-down-CjqstvxQ.js} +1 -1
  95. package/web-app/dist/assets/{trending-up-Djec3jIq.js → trending-up-CTyLjMzE.js} +1 -1
  96. package/web-app/dist/assets/{upload-BN8dXIlF.js → upload-BmRx-uv4.js} +1 -1
  97. package/web-app/dist/assets/{usePolling-Cu4yi04I.js → usePolling-ChHkSJye.js} +1 -1
  98. package/web-app/dist/assets/{user-IBI-k10o.js → user-ZXh_f35z.js} +1 -1
  99. package/web-app/dist/index.html +1 -1
  100. 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.
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.7
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.7 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
476
+ **v10.6.8 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 10.6.7
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'
@@ -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
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "10.6.7"
10
+ __version__ = "10.6.8"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -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.7
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 (36 tools), plugin marketplace |
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