loki-mode 9.17.0 → 9.17.2
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/issue-providers.sh +0 -24
- package/autonomy/lib/agent_readiness.py +1 -79
- package/autonomy/lib/outcome_ledger.py +0 -122
- package/autonomy/lib/proof-generator.py +4 -71
- package/autonomy/lib/verdict.py +4 -25
- package/autonomy/loki +5 -262
- package/autonomy/notify.sh +1 -70
- package/autonomy/queue-consumer.sh +18 -290
- package/autonomy/run.sh +65 -178
- package/autonomy/verify.sh +100 -12
- package/completions/_loki +0 -3
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +1 -168
- package/dashboard/static/index.html +222 -249
- package/docs/VERIFICATION-COST.md +20 -170
- package/loki-ts/dist/loki.js +311 -305
- package/mcp/__init__.py +1 -1
- package/mcp/_sdk_loader.py +0 -25
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/autonomy/lib/gate_policy.py +0 -166
- package/docs/QUEUE-OPERATIONS.md +0 -107
package/mcp/__init__.py
CHANGED
package/mcp/_sdk_loader.py
CHANGED
|
@@ -147,31 +147,6 @@ def _load_real_fastmcp():
|
|
|
147
147
|
for _k in ("mcp", "mcp.server"):
|
|
148
148
|
if _k in _saved_local:
|
|
149
149
|
sys.modules[_k] = _saved_local[_k]
|
|
150
|
-
elif _k in sys.modules:
|
|
151
|
-
# The SDK claimed a name the local package had NOT yet loaded,
|
|
152
|
-
# so there is no local module to re-pin and the old code left
|
|
153
|
-
# the SDK's module cached under it PERMANENTLY, process-wide.
|
|
154
|
-
#
|
|
155
|
-
# Measured (issue #190, python3.12 + SDK, from the repo root):
|
|
156
|
-
# any caller that reaches the loader before our own
|
|
157
|
-
# mcp/server.py is imported -- mcp/lsp_proxy.py, or a test that
|
|
158
|
-
# imports mcp._sdk_loader directly -- leaves the SDK's
|
|
159
|
-
# site-packages/mcp/server/__init__.py under "mcp.server". Every
|
|
160
|
-
# later `from mcp import server` in that process then silently
|
|
161
|
-
# gets the SDK instead of ours. That is how
|
|
162
|
-
# tests/test_mcp_agent_metrics_honesty.py skipped for its full
|
|
163
|
-
# life: it passed 7/7 alone but hit its capability guard in a
|
|
164
|
-
# full run, because tests/mcp/* ran first and poisoned the name.
|
|
165
|
-
#
|
|
166
|
-
# Deleting is correct where re-pinning is impossible: it leaves
|
|
167
|
-
# the name UNCLAIMED, so the next `import mcp.server` resolves
|
|
168
|
-
# through the local package exactly as it would have. Verified
|
|
169
|
-
# this does not disturb the SDK's own runtime subtree --
|
|
170
|
-
# mcp.types, mcp.server.lowlevel and mcp.server.fastmcp.* stay
|
|
171
|
-
# cached and FastMCP still instantiates, because FastMCP holds
|
|
172
|
-
# direct references to its dependencies rather than re-resolving
|
|
173
|
-
# "mcp.server" by name.
|
|
174
|
-
del sys.modules[_k]
|
|
175
150
|
for _k, _v in _saved_local.items():
|
|
176
151
|
# Restore any other purely-local submodules that the SDK import did
|
|
177
152
|
# not legitimately replace (e.g. mcp.magic_tools, mcp.tools).
|
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.17.
|
|
4
|
+
"version": "9.17.2",
|
|
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).",
|
|
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.17.
|
|
5
|
+
"version": "9.17.2",
|
|
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",
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Which gates block, which only advise, and what promoting one would have cost.
|
|
3
|
-
|
|
4
|
-
Ona's Veto Exec ships an audit-first ladder: "Start with audit rules, review
|
|
5
|
-
matches, then promote confirmed rules to block." The load-bearing part is the
|
|
6
|
-
MIDDLE step. A policy you cannot safely turn on is a policy nobody turns on, so
|
|
7
|
-
before flipping a gate to blocking you get to see what it WOULD have blocked.
|
|
8
|
-
|
|
9
|
-
We already had both ends and nothing in between: gates are advisory or blocking,
|
|
10
|
-
three promotion knobs exist (LOKI_GATE_MAGIC_DEBATE_BLOCKING, LOKI_COV_ENFORCE,
|
|
11
|
-
LOKI_POLICY_APPROVAL_ENFORCE), and .loki/quality/gate-failure-count.json has
|
|
12
|
-
counted per-gate failures the whole time. Nothing joined them, so an operator
|
|
13
|
-
deciding whether to promote a gate had to guess.
|
|
14
|
-
|
|
15
|
-
DETERMINISTIC. Reads two files and the environment. No model, no network, no
|
|
16
|
-
spend -- same repo state, same answer, and every number is a count a reader can
|
|
17
|
-
recompute by opening the same file.
|
|
18
|
-
|
|
19
|
-
NEVER PROMOTES ANYTHING. This reports; it does not change policy. Turning a gate
|
|
20
|
-
blocking stays an explicit operator act via the named environment variable,
|
|
21
|
-
because a tool that silently starts blocking is the thing operators most
|
|
22
|
-
reasonably fear.
|
|
23
|
-
|
|
24
|
-
Shape (assess() and --json, schema_version 1). This is the contract the
|
|
25
|
-
dashboard endpoint GET /api/gate-policy and tests/test_gate_policy_endpoint.py
|
|
26
|
-
both read against, so field names here are load-bearing:
|
|
27
|
-
|
|
28
|
-
schema_version int 1
|
|
29
|
-
status str "measured"
|
|
30
|
-
ledger str "present" | "absent" -- whether the per-gate failure
|
|
31
|
-
ledger .loki/quality/gate-failure-count.json was read
|
|
32
|
-
gates list one record per known gate, blocking gates first, each
|
|
33
|
-
group sorted by name:
|
|
34
|
-
gate str gate name (e.g. "code_review")
|
|
35
|
-
mode str "blocking" | "advisory" -- for a promotable gate this
|
|
36
|
-
depends on the ENVIRONMENT at call time
|
|
37
|
-
promotable bool True when a real promotion knob exists in run.sh
|
|
38
|
-
audit_hits int|null failures counted for this gate. null means
|
|
39
|
-
UNMEASURED -- no ledger, or no entry for this gate.
|
|
40
|
-
NEVER 0 for an unmeasured gate: 0 is the positive claim
|
|
41
|
-
that the gate ran and never fired, which is the false
|
|
42
|
-
green an absent measurement always produces.
|
|
43
|
-
why str one-line description of what the gate checks
|
|
44
|
-
promote_with str|null "VAR=value" to make an advisory gate blocking; null
|
|
45
|
-
when the gate already blocks
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
import json
|
|
49
|
-
import os
|
|
50
|
-
import sys
|
|
51
|
-
|
|
52
|
-
SCHEMA_VERSION = 1
|
|
53
|
-
|
|
54
|
-
# Gates that can be promoted from advisory to blocking, and the knob that does
|
|
55
|
-
# it. Only gates with a REAL knob in run.sh appear here -- listing an aspiration
|
|
56
|
-
# would tell an operator to set a variable nothing reads.
|
|
57
|
-
PROMOTABLE = {
|
|
58
|
-
"magic_debate": ("LOKI_GATE_MAGIC_DEBATE_BLOCKING", "true",
|
|
59
|
-
"spec-vs-implementation debate on generated modules"),
|
|
60
|
-
"test_coverage": ("LOKI_COV_ENFORCE", "1",
|
|
61
|
-
"project test runner pass/fail"),
|
|
62
|
-
"policy_approval": ("LOKI_POLICY_APPROVAL_ENFORCE", "1",
|
|
63
|
-
"staged-autonomy approval policy"),
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
# Gates that block unconditionally. Listed so the report is a complete picture
|
|
67
|
-
# rather than only the promotable subset -- an operator asking "what blocks here"
|
|
68
|
-
# should not have to read run.sh to find out.
|
|
69
|
-
ALWAYS_BLOCKING = {
|
|
70
|
-
"static_analysis": "CodeQL, ESLint/Pylint, type-checker findings on the diff",
|
|
71
|
-
"code_review": "3-reviewer blind review; Critical/High = BLOCK",
|
|
72
|
-
"mock_integrity": "tautological-assertion and mock-ratio detection",
|
|
73
|
-
"mutation_integrity": "assertion-churn (test-fitting) detection",
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def _counts(loki_dir):
|
|
78
|
-
"""Per-gate failure counts, or {} when the ledger has not been written."""
|
|
79
|
-
path = os.path.join(loki_dir, "quality", "gate-failure-count.json")
|
|
80
|
-
try:
|
|
81
|
-
with open(path) as fh:
|
|
82
|
-
data = json.load(fh)
|
|
83
|
-
return data if isinstance(data, dict) else {}
|
|
84
|
-
except (OSError, ValueError):
|
|
85
|
-
return {}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def assess(loki_dir=".loki", env=None):
|
|
89
|
-
env = os.environ if env is None else env
|
|
90
|
-
counts = _counts(loki_dir)
|
|
91
|
-
have_ledger = bool(counts)
|
|
92
|
-
|
|
93
|
-
gates = []
|
|
94
|
-
for name, why in sorted(ALWAYS_BLOCKING.items()):
|
|
95
|
-
gates.append({
|
|
96
|
-
"gate": name, "mode": "blocking", "promotable": False,
|
|
97
|
-
"audit_hits": counts.get(name) if have_ledger else None,
|
|
98
|
-
"why": why, "promote_with": None,
|
|
99
|
-
})
|
|
100
|
-
for name, (var, val, why) in sorted(PROMOTABLE.items()):
|
|
101
|
-
on = str(env.get(var, "")).lower() in ("1", "true", "yes")
|
|
102
|
-
gates.append({
|
|
103
|
-
"gate": name,
|
|
104
|
-
"mode": "blocking" if on else "advisory",
|
|
105
|
-
"promotable": True,
|
|
106
|
-
# None, not 0: an absent ledger means UNMEASURED, and reporting 0
|
|
107
|
-
# would read as "this gate never fired" -- the same false green a
|
|
108
|
-
# missing measurement always produces.
|
|
109
|
-
"audit_hits": counts.get(name) if have_ledger else None,
|
|
110
|
-
"why": why,
|
|
111
|
-
"promote_with": None if on else f"{var}={val}",
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
"schema_version": SCHEMA_VERSION,
|
|
116
|
-
"status": "measured",
|
|
117
|
-
"ledger": "present" if have_ledger else "absent",
|
|
118
|
-
"gates": gates,
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def render_text(res):
|
|
123
|
-
out = ["Gate policy -- what blocks here, and what only advises", ""]
|
|
124
|
-
for g in res["gates"]:
|
|
125
|
-
hits = g["audit_hits"]
|
|
126
|
-
if hits is None:
|
|
127
|
-
hit_s = "not measured"
|
|
128
|
-
elif hits == 0:
|
|
129
|
-
hit_s = "0 hits"
|
|
130
|
-
else:
|
|
131
|
-
hit_s = f"{hits} hit{'s' if hits != 1 else ''}"
|
|
132
|
-
mode = g["mode"].upper()
|
|
133
|
-
out.append(f" {mode:9} {g['gate']:20} {hit_s}")
|
|
134
|
-
out.append(f" {g['why']}")
|
|
135
|
-
if g["promote_with"]:
|
|
136
|
-
verb = "would have blocked" if (hits or 0) > 0 else "has not fired"
|
|
137
|
-
out.append(f" advisory: {verb} -- promote with {g['promote_with']}")
|
|
138
|
-
out.append("")
|
|
139
|
-
|
|
140
|
-
if res["ledger"] == "absent":
|
|
141
|
-
out.append(" No gate ledger yet (.loki/quality/gate-failure-count.json).")
|
|
142
|
-
out.append(" Counts read 'not measured' rather than 0: an absent")
|
|
143
|
-
out.append(" measurement is not evidence a gate never fired.")
|
|
144
|
-
else:
|
|
145
|
-
out.append(" Hits come from .loki/quality/gate-failure-count.json.")
|
|
146
|
-
out.append(" Open it and count the same numbers yourself.")
|
|
147
|
-
out.append("")
|
|
148
|
-
out.append(" This command never promotes a gate. Promotion is an explicit")
|
|
149
|
-
out.append(" operator act via the variable named above.")
|
|
150
|
-
return "\n".join(out)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
def main(argv):
|
|
154
|
-
as_json = "--json" in argv
|
|
155
|
-
loki_dir = ".loki"
|
|
156
|
-
for a in argv:
|
|
157
|
-
if not a.startswith("-"):
|
|
158
|
-
loki_dir = a
|
|
159
|
-
break
|
|
160
|
-
res = assess(loki_dir)
|
|
161
|
-
print(json.dumps(res, indent=2) if as_json else render_text(res))
|
|
162
|
-
return 0
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if __name__ == "__main__":
|
|
166
|
-
sys.exit(main(sys.argv[1:]))
|
package/docs/QUEUE-OPERATIONS.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Queue operations: what happens when a worker dies
|
|
2
|
-
|
|
3
|
-
Written for the person on call when builds stop coming out.
|
|
4
|
-
|
|
5
|
-
## The short version
|
|
6
|
-
|
|
7
|
-
A build that vanished is recoverable. Run:
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bash autonomy/queue-consumer.sh --reap
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
It requeues in-flight items whose worker died. It never touches a build that is
|
|
14
|
-
still running, and it never deletes anything.
|
|
15
|
-
|
|
16
|
-
## What the queue guarantees, and what it does not
|
|
17
|
-
|
|
18
|
-
| | redis backend | file backend |
|
|
19
|
-
|---|---|---|
|
|
20
|
-
| Delivery | at-least-once (default) | at-least-once |
|
|
21
|
-
| Crash leaves the item | in `<key>:processing` | in `processing/` |
|
|
22
|
-
| Automatic recovery | `--reap` | see below |
|
|
23
|
-
| Claim time recorded | `<key>:claims` hash | file mtime |
|
|
24
|
-
|
|
25
|
-
**A crashed worker does not lose the build.** It used to: the redis backend was
|
|
26
|
-
at-most-once (`LPOP`-then-run), so a worker killed between the pop and the
|
|
27
|
-
finish took the item with it and nothing anywhere recorded that it existed.
|
|
28
|
-
Proven against a real redis 8.6.3, then fixed with `LMOVE`, which pops and
|
|
29
|
-
records in-flight in one atomic step.
|
|
30
|
-
|
|
31
|
-
`LOKI_QUEUE_ACK=0` restores the old lossy behaviour if you are running a broker
|
|
32
|
-
that already guarantees delivery and do not want a second in-flight record.
|
|
33
|
-
|
|
34
|
-
## Recovering a stalled fleet
|
|
35
|
-
|
|
36
|
-
Symptom: items are queued, workers are up, and nothing completes.
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
# 1. What is stuck?
|
|
40
|
-
redis-cli -u "$LOKI_QUEUE_URL" LLEN loki-builds # waiting
|
|
41
|
-
redis-cli -u "$LOKI_QUEUE_URL" LLEN loki-builds:processing # claimed, in flight
|
|
42
|
-
|
|
43
|
-
# 2. Requeue anything whose worker is gone.
|
|
44
|
-
bash autonomy/queue-consumer.sh --reap
|
|
45
|
-
# -> reap: requeued 2, left 1 in flight (timeout 7200s)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The reaper reports what it did. "left 1 in flight" means one item's claim is
|
|
49
|
-
still inside the visibility timeout, so it is treated as a running build and not
|
|
50
|
-
touched.
|
|
51
|
-
|
|
52
|
-
### The timeout is the safety knob
|
|
53
|
-
|
|
54
|
-
`LOKI_QUEUE_VISIBILITY_SEC` (default 7200) is how long an item may be in flight
|
|
55
|
-
before the reaper assumes its worker died.
|
|
56
|
-
|
|
57
|
-
**It must exceed your longest legitimate build.** Set it too low and the reaper
|
|
58
|
-
requeues work that is still running, and the user gets the same build twice.
|
|
59
|
-
That is the only way this tool can hurt you, so it errs long by default.
|
|
60
|
-
|
|
61
|
-
### An item with no claim is reaped immediately
|
|
62
|
-
|
|
63
|
-
If a worker dies between claiming the item and stamping the claim time, the
|
|
64
|
-
item sits in `processing` with no entry in `<key>:claims`. The reaper treats
|
|
65
|
-
that as infinitely old, not infinitely young -- it is exactly the case that must
|
|
66
|
-
be recoverable, and erring the other way would strand the items this exists to
|
|
67
|
-
rescue.
|
|
68
|
-
|
|
69
|
-
## Running the reaper unattended
|
|
70
|
-
|
|
71
|
-
A CronJob is the intended shape. It is a separate mode rather than part of the
|
|
72
|
-
consume loop on purpose: a consumer that reaped on every poll would race its own
|
|
73
|
-
peers on a busy queue.
|
|
74
|
-
|
|
75
|
-
```yaml
|
|
76
|
-
schedule: "*/15 * * * *"
|
|
77
|
-
command: ["bash", "autonomy/queue-consumer.sh", "--reap"]
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## What is NOT claimed
|
|
81
|
-
|
|
82
|
-
- **The file backend has no automatic reaper.** `--reap` is redis-only today; a
|
|
83
|
-
crashed worker on the file backend leaves its item in `processing/` for a
|
|
84
|
-
human to move back to `pending/`. The item is not lost, but nothing recovers
|
|
85
|
-
it on its own.
|
|
86
|
-
- **No dead-letter queue.** An item that fails repeatedly is requeued
|
|
87
|
-
repeatedly. There is no automatic give-up-and-park.
|
|
88
|
-
- **`LREM` clears every equal copy.** Two identical specs queued twice both
|
|
89
|
-
clear on the first ack. Accepted deliberately: the alternative needs a
|
|
90
|
-
per-item token from the producer, and the failure mode here is one duplicate
|
|
91
|
-
re-drive rather than a lost build.
|
|
92
|
-
- **Only redis and file ship.** SQS, Pub/Sub, RabbitMQ and Kafka are
|
|
93
|
-
bring-your-own: override `queue.command` with your own consumer.
|
|
94
|
-
|
|
95
|
-
## Check any of this yourself
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
bash tests/test-queue-at-least-once.sh # 16 assertions; live half needs a broker
|
|
99
|
-
redis-server --port 6399 --save '' --appendonly no &
|
|
100
|
-
LOKI_TEST_REDIS_PORT=6399 bash tests/test-queue-at-least-once.sh
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
The live checks SKIP with a named reason when no broker is present rather than
|
|
104
|
-
passing on a mock. Both bugs found building this were invisible to a fake:
|
|
105
|
-
`redis-cli --no-raw` escapes the inner quotes of a JSON payload, which mangled
|
|
106
|
-
the item and made the ack a silent no-op. A green mock would have certified both
|
|
107
|
-
as working.
|