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/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.17.0'
78
+ __version__ = '9.17.2'
@@ -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.0",
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.0",
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:]))
@@ -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.