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 +2 -2
- package/VERSION +1 -1
- package/autonomy/lib/config-map.sh +24 -1
- package/autonomy/lib/proof-generator.py +99 -0
- package/autonomy/lib/proof-template.html +51 -1
- package/dashboard/__init__.py +1 -1
- package/docs/COMPETITIVE-NEXT-10.md +204 -0
- package/docs/INSTALLATION.md +1 -1
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
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.
|
|
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.
|
|
473
|
+
**v9.27.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
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
|
|
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) {
|
package/dashboard/__init__.py
CHANGED
|
@@ -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.
|
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:** v9.
|
|
5
|
+
**Version:** v9.27.0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
package/loki-ts/dist/loki.js
CHANGED
|
@@ -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.
|
|
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=
|
|
1337
|
+
//# debugId=61F0F6125299578AD6E69225C0CFE191
|
package/mcp/__init__.py
CHANGED
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.
|
|
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.
|
|
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",
|