loki-mode 9.26.1 → 9.27.0

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v9.26.1
6
+ # Loki Mode v9.27.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.26.1 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.27.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.26.1
1
+ 9.27.0
@@ -853,6 +853,22 @@ loki_config_unknown_keys() {
853
853
  local file="$1" fmt="$2"
854
854
  command -v python3 >/dev/null 2>&1 || return 0
855
855
 
856
+ # YAML needs a parser. The rest of this file reaches for yq first, so do the
857
+ # same: convert to JSON via yq and let the JSON walk below handle it. That
858
+ # keeps detection working on a host with yq but no pyyaml (CI installs
859
+ # neither by default, and yq is the more common of the two here). Without
860
+ # either parser the walk no-ops and YAML validates as it did before -- a
861
+ # missing parser must not invent a verdict.
862
+ local scratch_json=""
863
+ if [ "$fmt" = "yaml" ] && ! python3 -c "import yaml" >/dev/null 2>&1; then
864
+ command -v yq >/dev/null 2>&1 || return 0
865
+ scratch_json="$(mktemp "${TMPDIR:-/tmp}/loki-cfg-uk.XXXXXX")" || return 0
866
+ if ! yq eval -o=json '.' "$file" > "$scratch_json" 2>/dev/null; then
867
+ rm -f "$scratch_json"; return 0
868
+ fi
869
+ file="$scratch_json"; fmt="json"
870
+ fi
871
+
856
872
  local map_str="" mapping
857
873
  for mapping in "${LOKI_CONFIG_MAP[@]}"; do map_str+="${mapping%%:*}"$'\n'; done
858
874
 
@@ -912,7 +928,14 @@ def walk(node, prefix):
912
928
  walk(data, "")
913
929
  for u in unknown:
914
930
  print(u)
915
- ' 2>/dev/null || return 0
931
+ ' 2>/dev/null
932
+ # Always succeed: this helper reports keys on stdout, and a parser that
933
+ # cannot run must degrade to "nothing to report" rather than failing the
934
+ # caller. The `[ -n ... ] && rm` form would return non-zero on the common
935
+ # empty-scratch path and discard the captured output, so clean up with an
936
+ # unconditional rm on a possibly-empty path instead.
937
+ rm -f "${scratch_json:-/dev/null}" 2>/dev/null
938
+ return 0
916
939
  }
917
940
 
918
941
  loki_config_validate_file() {
@@ -644,6 +644,66 @@ def _collect_model(loki_dir, observed):
644
644
  return "unavailable"
645
645
 
646
646
 
647
+ def _collect_decisions(loki_dir):
648
+ """Which models actually ran, and did the configuration change mid-flight?
649
+
650
+ Reads the append-only trail at .loki/decisions/decisions.jsonl that
651
+ autonomy/lib/decision_record.py writes once per dispatch. That module
652
+ already computes the audit fact worth showing -- more than one model_id in
653
+ one project means the deciding component changed while the run was in
654
+ flight -- so this reuses summarize() rather than recomputing it.
655
+
656
+ THREE STATES, NEVER COLLAPSED, for the same reason _collect_model returns
657
+ the literal "unavailable" instead of guessing:
658
+
659
+ measured -- records exist; counts and model_changed are real
660
+ no_records -- the trail is absent for this run
661
+ unreadable -- the trail exists but could not be read
662
+
663
+ A section that renders nothing when the trail is absent reads as "no swap
664
+ happened", which is exactly the false green this receipt exists to prevent.
665
+ Absence of evidence is reported as absence of evidence.
666
+
667
+ model_changed is a DISCLOSED FACT, not a failure. run.sh documents four
668
+ legitimate reasons a model changes mid-run (opus-pin force, LOKI_MAX_TIER
669
+ clamp, mid-flight override, fable collapse). It never touches the verdict.
670
+
671
+ Corrupt lines are surfaced via unparseable_lines rather than dropped: an
672
+ audit trail that quietly discards what it cannot parse is worse than one
673
+ that admits the gap.
674
+ """
675
+ try:
676
+ from decision_record import summarize as _summarize
677
+ except ImportError:
678
+ return {"status": "unreadable", "reason": "decision_record module unavailable"}
679
+
680
+ try:
681
+ raw = _summarize(loki_dir)
682
+ except Exception as exc: # never let provenance reporting break the receipt
683
+ return {"status": "unreadable", "reason": str(exc)}
684
+
685
+ if not isinstance(raw, dict):
686
+ return {"status": "unreadable", "reason": "invalid summary"}
687
+
688
+ if raw.get("status") == "measured":
689
+ return {
690
+ "status": "measured",
691
+ "records": _to_int(raw.get("records"), 0),
692
+ "unparseable_lines": _to_int(raw.get("unparseable_lines"), 0),
693
+ "models": raw.get("models") or {},
694
+ "stages": raw.get("stages") or {},
695
+ "model_changed": bool(raw.get("model_changed")),
696
+ "temperature_changed": bool(raw.get("temperature_changed")),
697
+ # Never influences the verdict: this is provenance, not a gate.
698
+ "affects_verdict": False,
699
+ }
700
+
701
+ reason = str(raw.get("reason") or "unknown")
702
+ status = "no_records" if reason == "no_records" else "unreadable"
703
+ return {"status": status, "reason": reason,
704
+ "detail": str(raw.get("detail") or ""), "affects_verdict": False}
705
+
706
+
647
707
  def _collect_security(loki_dir):
648
708
  """Read .loki/quality/security-findings.json (the secure-by-default gate).
649
709
 
@@ -1237,6 +1297,9 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1237
1297
  termination = _collect_termination(loki_dir, args.session_exit_code)
1238
1298
  tests = _collect_tests(loki_dir)
1239
1299
  security = _collect_security(loki_dir)
1300
+ # Model provenance: which models actually ran, and did that change
1301
+ # mid-flight. Provenance only -- affects_verdict is False.
1302
+ decisions = _collect_decisions(loki_dir)
1240
1303
  functional = _collect_functional(loki_dir) # FV-2 record-half: descriptive only
1241
1304
  healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
1242
1305
  evidence_gate = _collect_evidence_gate(loki_dir)
@@ -1429,6 +1492,7 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1429
1492
  "diffs": diffs,
1430
1493
  "council": council,
1431
1494
  "quality_gates": quality_gates,
1495
+ "decisions": decisions,
1432
1496
  "cost": cost,
1433
1497
  "deployment": deployment,
1434
1498
  # Typed compatibility mirror for consumers that do not traverse facts.
@@ -1815,6 +1879,41 @@ def _render_fallback_html(proof):
1815
1879
  "who produced them, so this receipt trusts its generator. "
1816
1880
  "To sign future receipts, set LOKI_PROOF_GPG_KEY to a gpg "
1817
1881
  "key id (see docs/SIGNED-RECEIPTS.md).</p>")
1882
+
1883
+ # Model provenance, mirrored here for the same reason the signature row is:
1884
+ # this fallback "must not be quieter about provenance than the page it
1885
+ # stands in for". Three states, never collapsed -- an absent trail is
1886
+ # reported as absent, not rendered as silence that reads like "no change".
1887
+ dec = proof.get("decisions") or {}
1888
+ dstatus = dec.get("status")
1889
+ if dstatus == "measured":
1890
+ models = dec.get("models") or {}
1891
+ listing = ", ".join(
1892
+ "%s (%s)" % (esc(k2), esc(v2)) for k2, v2 in sorted(models.items())
1893
+ ) or "none recorded"
1894
+ if dec.get("model_changed"):
1895
+ change = ("The model CHANGED during this run. That is disclosed, not a "
1896
+ "fault -- a tier clamp, an operator override or a mid-flight "
1897
+ "failover all cause it legitimately.")
1898
+ else:
1899
+ change = "One model ran for the whole project; no mid-flight change."
1900
+ bad = _to_int(dec.get("unparseable_lines"), 0)
1901
+ badnote = ""
1902
+ if bad > 0:
1903
+ badnote = (" %d trail line(s) could not be parsed and are counted "
1904
+ "rather than dropped, so the list may be incomplete." % bad)
1905
+ rows.append("<p>Models dispatched (append-only decision trail): %s. %s%s "
1906
+ "Provenance only; never changes the verdict.</p>"
1907
+ % (listing, change, badnote))
1908
+ elif dstatus == "no_records":
1909
+ rows.append("<p>Models dispatched: no decision trail was recorded for this "
1910
+ "run, so per-iteration model attribution cannot be confirmed. "
1911
+ "Absence of the trail is reported as absence of evidence, not "
1912
+ "as proof that nothing changed.</p>")
1913
+ elif dstatus:
1914
+ rows.append("<p>Models dispatched: the decision trail could not be read "
1915
+ "(%s), so per-iteration model attribution is unavailable. "
1916
+ "Reported rather than omitted.</p>" % esc(dec.get("reason", "unknown")))
1818
1917
  red = proof.get("redaction", {})
1819
1918
  rows.append("<p>Redaction applied: %s (%s redactions, rules v%s)</p>" % (
1820
1919
  esc(red.get("applied")), esc(red.get("redactions_count")),
@@ -1268,10 +1268,60 @@
1268
1268
  var signNote = signed
1269
1269
  ? '<p class="hash-note">This receipt carries a detached GPG signature over the same canonical bytes that were hashed. A verifier holding the signer public key can confirm its provenance offline with <code>loki proof verify</code>.</p>'
1270
1270
  : '<p class="hash-note">This receipt is UNSIGNED, so it trusts the generator that produced it (<code>loki proof verify</code> reports <code>generator_trusted: true</code>). To sign future receipts, set <code>LOKI_PROOF_GPG_KEY</code> to a GPG key id before the run.</p>';
1271
+ // Model provenance from the append-only decision trail
1272
+ // (.loki/decisions/decisions.jsonl, written once per dispatch).
1273
+ //
1274
+ // The "Model" row above is the run-level model. It cannot answer the
1275
+ // question a regulated buyer actually asks: did the deciding component
1276
+ // change while the run was in flight? Two model ids in one project is
1277
+ // exactly that case, and the trail proves it either way.
1278
+ //
1279
+ // THREE STATES, NEVER COLLAPSED. A section that renders nothing when the
1280
+ // trail is absent reads as "no swap happened" -- the false green this
1281
+ // receipt exists to prevent. An older receipt predating this field has no
1282
+ // "decisions" key at all and is left silent rather than described, since a
1283
+ // receipt that never recorded the trail cannot honestly report on it.
1284
+ var dec = g(p, "decisions", null);
1285
+ var decNote = "";
1286
+ if (dec && g(dec, "status", "")) {
1287
+ var dstatus = g(dec, "status", "");
1288
+ if (dstatus === "measured") {
1289
+ var models = g(dec, "models", {}) || {};
1290
+ var names = [];
1291
+ for (var mk in models) {
1292
+ if (Object.prototype.hasOwnProperty.call(models, mk)) {
1293
+ names.push(esc(mk) + " (" + num(models[mk]) + ")");
1294
+ }
1295
+ }
1296
+ var changed = !!g(dec, "model_changed", false);
1297
+ var bad = num(g(dec, "unparseable_lines", 0));
1298
+ decNote = '<p class="hash-note">Models dispatched, from the run\u2019s own ' +
1299
+ "append-only decision trail: " + (names.length ? names.join(", ") : "none recorded") +
1300
+ ". " +
1301
+ (changed
1302
+ ? "The model CHANGED during this run. That is disclosed, not a fault -- a tier clamp, an " +
1303
+ "operator override or a mid-flight failover all cause it legitimately -- but it is the " +
1304
+ "thing you would otherwise have to take on trust."
1305
+ : "One model ran for the whole project; the trail shows no mid-flight change.") +
1306
+ (bad > 0
1307
+ ? " " + bad + " line" + (bad === 1 ? "" : "s") + " of the trail could not be parsed and " +
1308
+ "are counted here rather than dropped, so this list may be incomplete."
1309
+ : "") +
1310
+ " This is provenance and never changes the verdict.</p>";
1311
+ } else if (dstatus === "no_records") {
1312
+ decNote = '<p class="hash-note">No decision trail was recorded for this run, so which ' +
1313
+ "model served each iteration cannot be confirmed from this receipt. Absence of the trail " +
1314
+ "is reported as absence of evidence, not as proof that nothing changed.</p>";
1315
+ } else {
1316
+ decNote = '<p class="hash-note">The decision trail for this run could not be read (' +
1317
+ esc(g(dec, "reason", "unknown")) + "), so per-iteration model attribution is unavailable. " +
1318
+ "Reported rather than omitted.</p>";
1319
+ }
1320
+ }
1271
1321
  card.innerHTML = '<div class="kv">' + kv + "</div>" +
1272
1322
  (hash ? '<p class="hash-note">The integrity hash is a ' + esc(algo) +
1273
1323
  " digest of this proof. It proves the artifact has not been altered after it was emitted. It does not, by itself, prove who or what produced the work.</p>" : "") +
1274
- signNote;
1324
+ signNote + decNote;
1275
1325
  }
1276
1326
 
1277
1327
  function renderCta(p) {
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.26.1"
10
+ __version__ = "9.27.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -0,0 +1,204 @@
1
+ # The next items vs Factory.ai and 8090
2
+
3
+ Written 2026-09-09. Every item below cites a file:line, a command output, or a
4
+ fetched URL. Items that research proposed but that turned out to be **already
5
+ shipped** are listed at the bottom under "Not items" with the evidence, because
6
+ a plan that re-builds working code is worse than a shorter plan.
7
+
8
+ There are **seven** real items, not ten. Three of the research's candidates were
9
+ already implemented, and two more are architecturally unavailable to Loki as
10
+ designed. Padding to ten would mean inventing work.
11
+
12
+ ---
13
+
14
+ ## What the market actually rewards
15
+
16
+ Both competitors lead with **governance, not speed**. Factory.ai ($150M Series C,
17
+ Apr 2026) sells fleets of droids under "engineers governing how much autonomy
18
+ each workflow receives". 8090 ($135M Series A, EY partnership) sells a "governed
19
+ multiplayer platform under human-led oversight".
20
+
21
+ Loki's wedge is the same shape and sharper: **the only agent that hands you a
22
+ receipt you can check yourself.** The items below are ranked by how much each
23
+ one strengthens a claim a buyer can verify without trusting us.
24
+
25
+ ---
26
+
27
+ ## 1. Model provenance on the receipt -- SHIPPED v9.27.0
28
+
29
+ **Corrected.** An earlier draft of this item claimed the exogenous/advisory gate
30
+ split, files-changed and cost "do not reach the top of the receipt". That was
31
+ wrong, and reading the renderers disproved it: `proof-template.html:1165-1192`
32
+ already renders the split with the strongest honesty framing in the repo
33
+ ("a model that is confidently wrong scores itself green here"), plus
34
+ disabled-gate disclosure, cost, tokens, wall clock and files changed.
35
+
36
+ Five of the six fields were already shipped. **One was genuinely missing:**
37
+ per-iteration model attribution. `autonomy/lib/decision_record.py` writes an
38
+ append-only trail to `.loki/decisions/decisions.jsonl` once per dispatch, and
39
+ already computes the audit fact worth showing -- `model_changed`, meaning more
40
+ than one model id served one project. The proof generator read that file
41
+ **zero times**.
42
+
43
+ That is the question a regulated buyer actually asks, and the one Factory users
44
+ complain about: did the deciding component change mid-run without anyone saying
45
+ so? The run-level "Model" row cannot answer it.
46
+
47
+ **Shipped in v9.27.0** in both renderers, with three states never collapsed --
48
+ `measured`, `no_records`, `unreadable`. An absent trail is reported as absent
49
+ rather than rendered as silence that reads like "no swap happened", corrupt
50
+ trail lines are counted rather than dropped, and `affects_verdict` is `false`:
51
+ a mid-flight change is a disclosed fact (tier clamp, operator override,
52
+ failover all cause it legitimately), never a fault and never a verdict input.
53
+
54
+ ## 2. Make the machine contract discoverable
55
+
56
+ **Status: the contract exists and is documented; it is invisible where a user
57
+ would look.**
58
+
59
+ `docs/exit-codes.md` documents a genuinely good tiered contract: with
60
+ `LOKI_DURABLE_STATE=1`, `loki start` distinguishes "failed the quality gate"
61
+ from "crashed" -- written for a Kubernetes Job or an ECS task. `loki verify` has
62
+ its own documented contract (`docs/exit-codes.md:53`).
63
+
64
+ But `loki start --help` mentions `LOKI_DURABLE_STATE` **zero times** (measured).
65
+ A CI author reads `--help`, sees "0 on success, nonzero on failure", and builds
66
+ the coarse gate. Factory's `droid exec` advertises its exit codes in its own help
67
+ output; ours are a doc you have to already know exists.
68
+
69
+ **Do:** surface the durable contract in `loki start --help` and `loki verify
70
+ --help`, with a one-line pointer to `docs/exit-codes.md`.
71
+
72
+ **Note:** the research framed this as "Loki has no headless one-shot contract".
73
+ That framing was wrong -- the contract exists. The defect is discoverability,
74
+ which is a much cheaper fix.
75
+
76
+ ---
77
+
78
+ ## 3. Publish a measured kill-switch latency
79
+
80
+ **Status: mechanism exists, number does not.**
81
+
82
+ `check_human_intervention()` (`autonomy/run.sh`) implements PAUSE/STOP/INPUT.
83
+ There is no stated termination window anywhere in `docs/` (measured: zero
84
+ matches for "termination window" or "kill switch").
85
+
86
+ An enterprise buyer asks "how fast can I stop it?" Factory answers with a number.
87
+ "There is a stop signal" is not an answer.
88
+
89
+ **Do:** measure worst-case latency from signal to process exit across the bash
90
+ and Bun routes, publish the number, and add a test that fails if it regresses
91
+ past the published bound.
92
+
93
+ **Care:** publish the measured worst case, not the median. A number we beat 50%
94
+ of the time is worse than no number.
95
+
96
+ ---
97
+
98
+ ## 4. Close the config-diagnostic gap for the remaining format
99
+
100
+ **Status: shipped for JSON, `.env`, and YAML. Narrow residual gap only.**
101
+
102
+ `loki config validate` reports unknown keys in JSON and `.env` (v9.26.1) and in
103
+ YAML via pyyaml with a `yq` fallback (v9.26.2). `yq` is preinstalled on the
104
+ GitHub ubuntu-24.04 runner, and the fallback was verified against a stand-in
105
+ honouring both invocation shapes the real `yq` is called with, so CI and any
106
+ Linux host with either parser get full detection.
107
+
108
+ The residual gap is narrow: a host with **neither** pyyaml nor `yq` (a stock
109
+ macOS dev machine) gets no YAML detection. It degrades quietly, which is correct
110
+ -- a missing parser must never invent a verdict -- but silently.
111
+
112
+ **Do:** state the dependency in `loki config validate --help` so the gap is
113
+ visible rather than silent. Vendoring a YAML scanner is not worth it for one
114
+ host shape that already has a documented fallback available via `brew install
115
+ yq`.
116
+
117
+ ---
118
+
119
+ ## 5. `loki init` writes a config nothing reads
120
+
121
+ **Status: confirmed defect, low blast radius.**
122
+
123
+ `loki init` writes `.loki/loki.config.json` with six behavioral-looking keys --
124
+ `provider`, `complexity`, `quality_gates`, `parallel_mode`, `dashboard` -- and
125
+ labels it "project configuration" (`autonomy/loki:16598`). **Nothing reads any
126
+ of them** (measured: 0 read sites for all five; the only `template` match is a
127
+ comment). A user who sets `"quality_gates": false` is silently ignored.
128
+
129
+ Note this is a *different file* from `.loki/config.json`, which is live and is
130
+ read for `memory.disabled` and `otel_endpoint`.
131
+
132
+ **Mitigated already:** as of v9.26.1, `loki config validate` on that file
133
+ reports each dead key by name. The gap is now diagnosable rather than silent.
134
+
135
+ **Do:** the honest minimum is to stop describing it as "project configuration".
136
+ Removing the keys outright flips `tests/test-init-command.sh:141`, which asserts
137
+ `'provider' in d` -- so that is a deliberate contract change, not a cleanup.
138
+
139
+ ---
140
+
141
+ ## 6. Verdict influence for the LLM review stage
142
+
143
+ **Status: stage ships in v9.26.0/9.26.1; verdict influence deliberately deferred.**
144
+
145
+ `llm_review` now runs by default and is recorded, but `affects_verdict` is
146
+ `false`. That was the right call for the release -- flipping it would silently
147
+ break anyone gating CI on exit 0.
148
+
149
+ **Do:** measure the reviewer on real diffs, then promote verdict influence behind
150
+ an explicit flag (`--llm-blocks`) before considering it as a default. The
151
+ sequencing matters more than the speed: a reviewer that returns CONCERNS where
152
+ deterministic-only returned VERIFIED is a breaking change to a published exit
153
+ contract.
154
+
155
+ ---
156
+
157
+ ## 7. Make the deferred-suite gap structural
158
+
159
+ **Status: process defect, cost two broken releases this cycle.**
160
+
161
+ v9.25.0 and v9.25.1 both failed to publish because a check that guards the
162
+ shipped artifact was **deferred by the fast tier** -- the only tier that runs
163
+ before every push. v9.26.0 failed the same way on repo-wide ShellCheck.
164
+
165
+ CLAUDE.md already states the rule ("a check that guards the shipped artifact must
166
+ run in the FAST tier"). The rule is not enforced.
167
+
168
+ **Shipped in v9.26.3** as `scripts/guard-changed.sh`: runs the suites that
169
+ reference the files in your diff, plus ShellCheck on the changed shell files.
170
+ Measured 8s for a one-file change, ~135s worst case, against 26m50s for the FULL
171
+ tier. Verified it would have blocked the v9.26.0 push locally.
172
+
173
+ **Remaining:** it is necessary, not sufficient. It cannot catch a failure that
174
+ depends on CI differing from your machine -- which is exactly how v9.26.2 failed,
175
+ on a suite this script selects and runs. The follow-on work is making
176
+ environment-conditional assertions name their condition (`command -v yq`) rather
177
+ than assume the author's host. That is a review habit, not a script.
178
+
179
+ ---
180
+
181
+ ## Not items (research proposed these; they are already shipped)
182
+
183
+ - **Signed receipts default-off is a moat gated behind an env var.** The receipt
184
+ already states signature status explicitly in both directions
185
+ (`proof-generator.py:1808-1820`): SIGNED, or UNSIGNED with the honest line that
186
+ the integrity hash "does NOT prove who produced them, so this receipt trusts
187
+ its generator." That is the shippable version. Do **not** flip
188
+ `LOKI_PROOF_GPG_KEY` to default-on: with no key present it would either fail
189
+ the run or silently emit no signature, and the second is the false-green this
190
+ project exists to prevent. Key distribution is a founder decision.
191
+ - **OTEL to a customer-owned collector.** Already implemented -- `otel_endpoint`
192
+ is persisted and read (`autonomy/loki:26755`, `:26828-26837`).
193
+ - **A headless exec contract with documented exit codes.** Already exists via
194
+ `LOKI_DURABLE_STATE=1`; see item 2, which is the real (smaller) gap.
195
+
196
+ ## Architecturally unavailable, and worth saying so
197
+
198
+ - **Per-command risk tiers** and a **hard command blocklist** ("cannot be
199
+ bypassed by approval", per Factory's docs). `autonomy/run.sh:515` documents
200
+ that `LOKI_ALLOWED_PATHS` "does NOT restrict provider-driven agent writes
201
+ (run.sh never sees them)". You cannot classify a command you never observe.
202
+ The only honest form is a sandbox-boundary blocklist, which is a different and
203
+ much larger piece of work. Attempting a partial version would ship a security
204
+ claim we cannot keep.
@@ -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:** v9.26.1
5
+ **Version:** v9.27.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var St=Object.create;var{getPrototypeOf:yt,defineProperty:jG,getOwnPropertyNames:bt}=Object;var ft=Object.prototype.hasOwnProperty;function _t($){return this[$]}var vt,ht,gt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?vt??=new WeakMap:ht??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?St(yt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of bt($))if(!ft.call(J,q))jG(J,q,{get:_t.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var mt=($)=>$;function ut($,X){this[$]=mt.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:ut.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var YR={};B1(YR,{lokiDir:()=>h0,homeLokiDir:()=>HQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as dt}from"url";import{existsSync as Zq}from"fs";import{homedir as pt}from"os";function ct(){let $=VR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(VR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function HQ(){return h2(pt(),".loki")}var VR,L1;var k1=s(()=>{VR=LG(dt(import.meta.url));L1=ct()});import{readFileSync as lt}from"fs";import{resolve as it,dirname as at}from"path";import{fileURLToPath as ot}from"url";function j9(){if(n3!==null)return n3;let $="9.26.1";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=at(ot(import.meta.url)),Q=AG(X);n3=lt(it(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var GR={};B1(GR,{runOrThrow:()=>Ue,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>We,commandExists:()=>g5,ShellError:()=>CG,MAX_STDOUT_BYTES:()=>WR});async function Jq($,X=WR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Ue($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new CG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=He($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function He($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function We($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var WR=16777216,CG;var y8=s(()=>{CG=class CG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Ge?"":$}var Ge,p0,$5,q1,L61,A1,f1,m5,r;var t7=s(()=>{Ge=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),L61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as De}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(De($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var yR={};B1(yR,{runStatus:()=>oe});import{existsSync as u5,readFileSync as A9,readdirSync as RR,statSync as IR}from"fs";import{resolve as A5,basename as ge}from"path";import{homedir as me}from"os";function wR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function PR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=wR($),Y=wR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function de(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
2
+ var St=Object.create;var{getPrototypeOf:yt,defineProperty:jG,getOwnPropertyNames:bt}=Object;var ft=Object.prototype.hasOwnProperty;function _t($){return this[$]}var vt,ht,gt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?vt??=new WeakMap:ht??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?St(yt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of bt($))if(!ft.call(J,q))jG(J,q,{get:_t.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var mt=($)=>$;function ut($,X){this[$]=mt.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:ut.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var YR={};B1(YR,{lokiDir:()=>h0,homeLokiDir:()=>HQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as dt}from"url";import{existsSync as Zq}from"fs";import{homedir as pt}from"os";function ct(){let $=VR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(VR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function HQ(){return h2(pt(),".loki")}var VR,L1;var k1=s(()=>{VR=LG(dt(import.meta.url));L1=ct()});import{readFileSync as lt}from"fs";import{resolve as it,dirname as at}from"path";import{fileURLToPath as ot}from"url";function j9(){if(n3!==null)return n3;let $="9.27.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=at(ot(import.meta.url)),Q=AG(X);n3=lt(it(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var GR={};B1(GR,{runOrThrow:()=>Ue,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>We,commandExists:()=>g5,ShellError:()=>CG,MAX_STDOUT_BYTES:()=>WR});async function Jq($,X=WR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Ue($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new CG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=He($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function He($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function We($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var WR=16777216,CG;var y8=s(()=>{CG=class CG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Ge?"":$}var Ge,p0,$5,q1,L61,A1,f1,m5,r;var t7=s(()=>{Ge=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),L61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as De}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(De($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var yR={};B1(yR,{runStatus:()=>oe});import{existsSync as u5,readFileSync as A9,readdirSync as RR,statSync as IR}from"fs";import{resolve as A5,basename as ge}from"path";import{homedir as me}from"os";function wR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function PR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=wR($),Y=wR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function de(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -1334,4 +1334,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1334
1334
  `),2}case"start":{let{runStart:z}=await Promise.resolve().then(() => (Et(),Pt));return z(Q)}default:return process.stderr.write(`Unknown command: ${X}
1335
1335
  `),process.stderr.write(xt),2}}DR();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var X61=await $61(Bun.argv.slice(2));process.exit(X61);
1336
1336
 
1337
- //# debugId=B98225BA72BA8822593D774679C4FF5C
1337
+ //# debugId=61F0F6125299578AD6E69225C0CFE191
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.26.1'
78
+ __version__ = '9.27.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "9.26.1",
4
+ "version": "9.27.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider, opencode).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "9.26.1",
5
+ "version": "9.27.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",