loki-mode 9.12.6 → 9.17.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.
Files changed (39) hide show
  1. package/README.md +81 -101
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/intent.sh +414 -0
  5. package/autonomy/issue-providers.sh +24 -0
  6. package/autonomy/lib/agent_readiness.py +280 -0
  7. package/autonomy/lib/claim_grounding.py +171 -0
  8. package/autonomy/lib/config-map.sh +10 -6
  9. package/autonomy/lib/decision_record.py +198 -0
  10. package/autonomy/lib/failure_memory.py +199 -0
  11. package/autonomy/lib/gate_policy.py +166 -0
  12. package/autonomy/lib/outcome_ledger.py +620 -0
  13. package/autonomy/lib/preedit_snapshot.py +216 -0
  14. package/autonomy/lib/proof-generator.py +71 -4
  15. package/autonomy/lib/verdict.py +204 -0
  16. package/autonomy/loki +430 -15
  17. package/autonomy/notify.sh +70 -1
  18. package/autonomy/provider-offer.sh +25 -1
  19. package/autonomy/queue-consumer.sh +290 -18
  20. package/autonomy/run.sh +527 -12
  21. package/autonomy/telemetry.sh +8 -1
  22. package/completions/_loki +5 -0
  23. package/completions/loki.bash +2 -1
  24. package/dashboard/__init__.py +1 -1
  25. package/dashboard/run.py +13 -2
  26. package/dashboard/scim.py +221 -0
  27. package/dashboard/server.py +190 -1
  28. package/dashboard/static/index.html +248 -55
  29. package/docs/GATE-FAILURE-TRIAGE.md +254 -0
  30. package/docs/LOOP-CANDIDATE-PROPOSAL-v1.md +167 -0
  31. package/docs/LOOP-HARNESS-AUDIT.md +53 -0
  32. package/docs/QUEUE-OPERATIONS.md +107 -0
  33. package/docs/VERIFICATION-COST.md +273 -0
  34. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  35. package/loki-ts/dist/loki.js +414 -416
  36. package/mcp/__init__.py +1 -1
  37. package/mcp/_sdk_loader.py +25 -0
  38. package/package.json +1 -1
  39. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
@@ -194,7 +194,14 @@ _loki_known_command() {
194
194
  # a first run, never the user's paths, versions, or hostnames.
195
195
  _loki_known_blocker() {
196
196
  case "${1:-}" in
197
- no_provider|node|python3|jq|git|curl|disk|skill_symlink)
197
+ # not_logged_in is distinct from no_provider on purpose. They are the two
198
+ # halves of the same wall and they need opposite fixes: no_provider means
199
+ # "install something", not_logged_in means "authenticate the thing you
200
+ # already installed". Collapsing the second into `other` would make the
201
+ # single most common post-install failure unactionable in the data --
202
+ # and it is the worst-placed one, since without this the user only learns
203
+ # of it after confirming the spend.
204
+ no_provider|not_logged_in|node|python3|jq|git|curl|disk|skill_symlink)
198
205
  printf '%s' "$1" ;;
199
206
  *)
200
207
  printf 'other' ;;
package/completions/_loki CHANGED
@@ -120,6 +120,7 @@ function _loki {
120
120
  function _loki_commands {
121
121
  local -a commands
122
122
  commands=(
123
+ 'gates:What blocks here vs only advises, and the promotion knob'
123
124
  'start:Start Loki Mode'
124
125
  'quick:Quick single-task mode'
125
126
  'quickstart:Guided first build from your idea'
@@ -171,6 +172,9 @@ function _loki_commands {
171
172
  'metrics:Session productivity report'
172
173
  'share:Share session report as GitHub Gist'
173
174
  'proof:Inspect/share proof-of-run artifacts'
175
+ 'outcomes:What happened to the work AFTER the receipt (reverted/reworked/survived)'
176
+ 'verdict:The five measured trust signals in one readable block'
177
+ 'readiness:Can an agent verify its own work in this repo? (measured, not LLM-scored)'
174
178
  'preview:Preview the locally-running app'
175
179
  'deploy:Deploy the built product (CI/CD-aware)'
176
180
  'context:Context window management'
@@ -190,6 +194,7 @@ function _loki_commands {
190
194
  'magic:Spec-driven component generation'
191
195
  'assets:Export/import shareable team assets'
192
196
  'spec:Living spec drift detection'
197
+ 'intent:Does the spec still say what was actually wanted'
193
198
  'verify:Deterministic PR verification'
194
199
  'grill:Interrogate a spec before building'
195
200
  'trust:Visible trust trajectory'
@@ -5,7 +5,8 @@ _loki_completion() {
5
5
  _init_completion || return
6
6
 
7
7
  # Main subcommands (must match autonomy/loki main case statement)
8
- local main_commands="start quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover remote deploy docker mcp magic assets analyze compliance crash open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help"
8
+ local main_commands="start quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt outcomes verdict readiness explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover gates remote deploy docker mcp magic assets analyze compliance crash open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help"
9
+ local main_commands="start quick monitor demo tour welcome init stop pause resume steer status next ship dashboard web serve api sandbox notify import github issue intent config provider reset memory compound checkpoint council dogfood projects enterprise secrets cockpit secure own handoff doctor watchdog audit metrics syslog onboard share proof receipt explain plan report cost estimate kpis stats test ci watch telemetry agent context ctx code run export review optimize heal modernize migrate cluster worktree wt trigger failover gates remote deploy docker mcp magic assets analyze compliance crash open otel preview quickstart rc rollback self-update sentrux setup-skill spec state template trust trust-metrics ultracode update verify voice why wiki bench cleanup logs grill docs cp version completions help"
9
10
 
10
11
  # 1. If we are on the first argument (subcommand)
11
12
  if [[ $cword -eq 1 ]]; then
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.12.6"
10
+ __version__ = "9.17.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
package/dashboard/run.py CHANGED
@@ -8,15 +8,26 @@ Usage:
8
8
  """
9
9
 
10
10
  import argparse
11
+ import os
11
12
  import sys
12
13
 
13
14
 
14
15
  def main():
15
16
  parser = argparse.ArgumentParser(description="Loki Mode Dashboard Server")
17
+ # Loopback by default, matching server.py:run_server and control.py. This
18
+ # launcher defaulted to 0.0.0.0, which published an UNAUTHENTICATED control
19
+ # plane on every interface: dashboard auth is opt-in
20
+ # (LOKI_ENTERPRISE_AUTH, default false in dashboard/auth.py), so with it
21
+ # unset -- the default -- anyone routable to the host could reach endpoints
22
+ # that stop builds and read receipts. A default must be safe on its own;
23
+ # exposing the network is a decision an operator makes explicitly, so
24
+ # --host 0.0.0.0 and LOKI_DASHBOARD_HOST both still work for the container
25
+ # and Helm paths that genuinely need it.
16
26
  parser.add_argument(
17
27
  "--host",
18
- default="0.0.0.0",
19
- help="Host to bind to (default: 0.0.0.0)",
28
+ default=os.environ.get("LOKI_DASHBOARD_HOST", "127.0.0.1"),
29
+ help="Host to bind to (default: $LOKI_DASHBOARD_HOST or 127.0.0.1). "
30
+ "Use 0.0.0.0 only with authentication enabled.",
20
31
  )
21
32
  parser.add_argument(
22
33
  "--port",
@@ -0,0 +1,221 @@
1
+ """SCIM 2.0 user provisioning: deprovisioning that actually revokes access.
2
+
3
+ WHY THIS EXISTS. Measured against our own code before writing any: OIDC is real
4
+ here (76 references in dashboard/auth.py) and SAML and SCIM are both ZERO. That
5
+ is the whole enterprise identity gap -- not authentication as a category, which
6
+ we have, but PROVISIONING, which we do not.
7
+
8
+ It matters more than its size suggests. With OIDC alone, an employee removed
9
+ from the IdP stops being able to LOG IN, but any token already issued to them
10
+ keeps working until it expires. For a buyer whose security review asks "show me
11
+ that a terminated employee loses access within minutes", that is the answer that
12
+ fails the review. SCIM is the protocol that answers it.
13
+
14
+ WORTH KNOWING ABOUT THE COMPETITION. Factory ships SCIM. Devin's SCIM is
15
+ documented only for Devin DESKTOP -- the page instructs you to configure it at
16
+ windsurf.com/team/settings under the app name "Windsurf" -- and no page documents
17
+ SCIM provisioning for Devin CLOUD sessions, which get SAML/OIDC plus IdP-group
18
+ sync only. So this is table stakes against one competitor and a genuine gap in
19
+ the other.
20
+
21
+ WHAT THIS IS NOT. It is not a second identity system. It maps IdP group
22
+ membership onto the scope hierarchy dashboard/auth.py already enforces
23
+ (* -> control -> write -> read), so there is exactly one authorization model and
24
+ SCIM only decides who holds which scope. A parallel permission model would
25
+ eventually disagree with the real one, and the disagreement would be invisible
26
+ until an audit.
27
+
28
+ DEPROVISIONING IS SOFT BY DEFAULT, AND THAT IS DELIBERATE. `active: false`
29
+ revokes access immediately but PRESERVES the record, because an audit trail that
30
+ loses the identity of whoever performed past actions is not an audit trail. Hard
31
+ delete is available and separate, so destroying history is always an explicit act.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ import os
38
+ from datetime import datetime, timezone
39
+ from typing import Optional
40
+
41
+ SCIM_SCHEMA_USER = "urn:ietf:params:scim:schemas:core:2.0:User"
42
+ SCIM_SCHEMA_LIST = "urn:ietf:params:scim:api:messages:2.0:ListResponse"
43
+ SCIM_SCHEMA_ERROR = "urn:ietf:params:scim:api:messages:2.0:Error"
44
+
45
+ # IdP group -> our scope. Mirrors the hierarchy in dashboard/auth.py rather than
46
+ # inventing a second one: * grants everything, control grants write and read.
47
+ # An unrecognised group maps to "read", never to a higher scope -- an unknown
48
+ # group is not evidence of privilege, and defaulting upward is how a
49
+ # misconfigured IdP silently grants admin.
50
+ GROUP_SCOPE_MAP = {
51
+ "loki-admins": "*",
52
+ "loki-owners": "*",
53
+ "loki-operators": "control",
54
+ "loki-writers": "write",
55
+ "loki-readers": "read",
56
+ }
57
+ DEFAULT_SCOPE = "read"
58
+
59
+
60
+ def _now():
61
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
62
+
63
+
64
+ def _store_path(loki_dir):
65
+ return os.path.join(loki_dir, "scim", "users.json")
66
+
67
+
68
+ def _load(loki_dir):
69
+ p = _store_path(loki_dir)
70
+ if not os.path.isfile(p):
71
+ return {}
72
+ try:
73
+ with open(p, "r", encoding="utf-8") as fh:
74
+ return json.load(fh)
75
+ except (OSError, ValueError):
76
+ # A corrupt store must not silently become an empty one: returning {}
77
+ # here would read as "no users provisioned" and could re-grant access to
78
+ # someone who was deprovisioned. Signalled to the caller instead.
79
+ return None
80
+
81
+
82
+ def _save(loki_dir, data):
83
+ p = _store_path(loki_dir)
84
+ os.makedirs(os.path.dirname(p), exist_ok=True)
85
+ tmp = p + ".tmp"
86
+ with open(tmp, "w", encoding="utf-8") as fh:
87
+ json.dump(data, fh, indent=2, sort_keys=True)
88
+ fh.write("\n")
89
+ # Atomic replace: a half-written user store is an access-control decision
90
+ # made from corrupt data.
91
+ os.replace(tmp, p)
92
+
93
+
94
+ def scope_for_groups(groups):
95
+ """Highest scope any of the user's groups grants.
96
+
97
+ Highest-wins, because a user in both loki-readers and loki-admins is an
98
+ admin. Unknown groups contribute DEFAULT_SCOPE and never more.
99
+ """
100
+ rank = {"read": 1, "write": 2, "control": 3, "*": 4}
101
+ best = DEFAULT_SCOPE
102
+ for g in groups or []:
103
+ s = GROUP_SCOPE_MAP.get(str(g).strip().lower(), DEFAULT_SCOPE)
104
+ if rank.get(s, 1) > rank.get(best, 1):
105
+ best = s
106
+ return best
107
+
108
+
109
+ def provision(loki_dir, user_name, external_id=None, groups=None, active=True):
110
+ """Create or update a user. Returns a SCIM User resource."""
111
+ if not user_name:
112
+ return {"schemas": [SCIM_SCHEMA_ERROR], "status": "400",
113
+ "detail": "userName is required"}
114
+
115
+ data = _load(loki_dir)
116
+ if data is None:
117
+ return {"schemas": [SCIM_SCHEMA_ERROR], "status": "500",
118
+ "detail": "user store is unreadable; refusing to make an access "
119
+ "decision from corrupt data"}
120
+
121
+ uid = external_id or user_name
122
+ existing = data.get(uid, {})
123
+ rec = {
124
+ "id": uid,
125
+ "userName": user_name,
126
+ "externalId": external_id or "",
127
+ "groups": list(groups or []),
128
+ "scope": scope_for_groups(groups),
129
+ "active": bool(active),
130
+ "created": existing.get("created") or _now(),
131
+ "lastModified": _now(),
132
+ }
133
+ data[uid] = rec
134
+ _save(loki_dir, data)
135
+ return to_scim_user(rec)
136
+
137
+
138
+ def deprovision(loki_dir, uid, hard=False):
139
+ """Revoke access. Soft by default so the audit trail survives.
140
+
141
+ An audit log that cannot say WHO performed an action is not an audit log, so
142
+ a departed user's record is retained by default and merely marked inactive.
143
+ Hard delete exists and is separate, so destroying history is always explicit.
144
+ """
145
+ data = _load(loki_dir)
146
+ if data is None:
147
+ return {"schemas": [SCIM_SCHEMA_ERROR], "status": "500",
148
+ "detail": "user store is unreadable"}
149
+ if uid not in data:
150
+ return {"schemas": [SCIM_SCHEMA_ERROR], "status": "404",
151
+ "detail": f"no such user: {uid}"}
152
+
153
+ if hard:
154
+ data.pop(uid)
155
+ _save(loki_dir, data)
156
+ return {"status": "204", "deleted": uid, "audit_trail_preserved": False}
157
+
158
+ data[uid]["active"] = False
159
+ data[uid]["lastModified"] = _now()
160
+ _save(loki_dir, data)
161
+ return {"status": "200", "deactivated": uid, "audit_trail_preserved": True}
162
+
163
+
164
+ def is_active(loki_dir, uid):
165
+ """Authorization answer for one user. Fails CLOSED on any doubt.
166
+
167
+ An unreadable store, an unknown user, and an inactive user all return False.
168
+ A provisioning system that fails open is worse than none, because it creates
169
+ the belief that deprovisioning works.
170
+ """
171
+ data = _load(loki_dir)
172
+ if data is None:
173
+ return False
174
+ rec = data.get(uid)
175
+ if not rec:
176
+ return False
177
+ return bool(rec.get("active"))
178
+
179
+
180
+ def effective_scope(loki_dir, uid):
181
+ """The scope a user actually holds, or None when they hold none."""
182
+ data = _load(loki_dir)
183
+ if data is None:
184
+ return None
185
+ rec = data.get(uid)
186
+ if not rec or not rec.get("active"):
187
+ return None
188
+ return rec.get("scope") or DEFAULT_SCOPE
189
+
190
+
191
+ def to_scim_user(rec):
192
+ return {
193
+ "schemas": [SCIM_SCHEMA_USER],
194
+ "id": rec["id"],
195
+ "userName": rec["userName"],
196
+ "externalId": rec.get("externalId", ""),
197
+ "active": rec.get("active", False),
198
+ "groups": [{"value": g} for g in rec.get("groups", [])],
199
+ "meta": {
200
+ "resourceType": "User",
201
+ "created": rec.get("created"),
202
+ "lastModified": rec.get("lastModified"),
203
+ },
204
+ # Not part of the SCIM spec, surfaced because an operator reading this
205
+ # response needs to see what the group mapping actually granted rather
206
+ # than deriving it.
207
+ "lokiScope": rec.get("scope"),
208
+ }
209
+
210
+
211
+ def list_users(loki_dir, active_only=False):
212
+ data = _load(loki_dir)
213
+ if data is None:
214
+ return {"schemas": [SCIM_SCHEMA_ERROR], "status": "500",
215
+ "detail": "user store is unreadable"}
216
+ recs = [r for r in data.values() if (not active_only or r.get("active"))]
217
+ return {
218
+ "schemas": [SCIM_SCHEMA_LIST],
219
+ "totalResults": len(recs),
220
+ "Resources": [to_scim_user(r) for r in sorted(recs, key=lambda r: r["id"])],
221
+ }
@@ -1224,6 +1224,28 @@ class WebSocketBoundaryMiddleware:
1224
1224
  # accept-then-drop, which reads to a client as a flaky network.
1225
1225
  await send({"type": "websocket.close", "code": 1008})
1226
1226
  return
1227
+ # Cross-Site WebSocket Hijacking. The address check above proves the
1228
+ # PEER is local; it says nothing about who told that peer to connect.
1229
+ # A browser on any origin can open ws://127.0.0.1:57374/ws -- the
1230
+ # request comes FROM the loopback interface and passes the check
1231
+ # above, and CORS does not apply to WebSocket upgrades, so no
1232
+ # existing control stops it. /ws/collab is writable, so this is not
1233
+ # read-only exposure.
1234
+ #
1235
+ # Same-origin browsers send Origin; non-browser clients (CLI, tests,
1236
+ # health probes) send none. An ABSENT Origin is therefore allowed --
1237
+ # it cannot be forged by a page, since browsers always attach it --
1238
+ # while a PRESENT one must be in the allowlist the HTTP path already
1239
+ # uses. That keeps every scripted client working and closes the
1240
+ # browser-driven path, without a second source of truth for origins.
1241
+ origin = None
1242
+ for raw_name, raw_value in scope.get("headers", []):
1243
+ if raw_name == b"origin":
1244
+ origin = raw_value.decode("latin-1").strip()
1245
+ break
1246
+ if origin and "*" not in _cors_origins and origin not in _cors_origins:
1247
+ await send({"type": "websocket.close", "code": 1008})
1248
+ return
1227
1249
  await self.app(scope, receive, send)
1228
1250
 
1229
1251
 
@@ -8332,6 +8354,93 @@ async def get_trust_trajectory():
8332
8354
  return traj
8333
8355
 
8334
8356
 
8357
+ # =============================================================================
8358
+ # Gate policy API: which gates block here, and which only advise?
8359
+ # =============================================================================
8360
+
8361
+ _GATE_POLICY_MODULE = None # cached import of autonomy/lib/gate_policy.py
8362
+
8363
+
8364
+ def _load_gate_policy_module():
8365
+ """Import the shared gate-policy reporter (single source of truth).
8366
+
8367
+ Same shape as _load_trust_module above: the derivation lives in
8368
+ autonomy/lib/gate_policy.py so this endpoint, the bash `loki gates`, and the
8369
+ test suite all agree, and it loads via importlib because autonomy/lib is not
8370
+ an importable package. Cached after first load, None when unavailable.
8371
+ """
8372
+ global _GATE_POLICY_MODULE
8373
+ if _GATE_POLICY_MODULE is not None:
8374
+ return _GATE_POLICY_MODULE
8375
+ repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
8376
+ mod_path = os.path.join(repo_root, "autonomy", "lib", "gate_policy.py")
8377
+ if not os.path.isfile(mod_path):
8378
+ return None
8379
+ try:
8380
+ import importlib.util as _ilu
8381
+ spec = _ilu.spec_from_file_location("gate_policy", mod_path)
8382
+ if spec is None or spec.loader is None:
8383
+ return None
8384
+ mod = _ilu.module_from_spec(spec)
8385
+ spec.loader.exec_module(mod)
8386
+ _GATE_POLICY_MODULE = mod
8387
+ return mod
8388
+ except Exception:
8389
+ return None
8390
+
8391
+
8392
+ @app.get("/api/gate-policy", dependencies=[Depends(auth.require_scope("read"))])
8393
+ async def get_gate_policy():
8394
+ """Which quality gates block on this repo, and which only advise.
8395
+
8396
+ Read-only by construction. There is deliberately no POST counterpart:
8397
+ promoting an advisory gate to blocking stays an explicit operator act via
8398
+ the environment variable named in each record's `promote_with`, because a
8399
+ surface that can silently start blocking is the thing operators most
8400
+ reasonably fear. This endpoint reports; it never promotes.
8401
+
8402
+ Honest-data rule, and the reason `audit_hits` is nullable: an unmeasured
8403
+ gate reports null, NEVER 0. Zero is the positive claim that the gate ran and
8404
+ never fired; an absent ledger is not evidence of that. See the shape
8405
+ contract in the gate_policy module docstring.
8406
+
8407
+ Fails open like /api/trust/trajectory: a missing module returns a renderable
8408
+ available=False payload rather than a 500. Unlike that sibling the assess()
8409
+ call is also wrapped, because it reads a ledger file that an unrelated
8410
+ writer can leave malformed, and a broken ledger must not take down a
8411
+ read-only report.
8412
+
8413
+ Response keys are the module shape contract plus `available` (bool). On the
8414
+ fail-open path `available` is False, `status` is "unavailable" (a value the
8415
+ module contract does not itself define, since assess() never returned), an
8416
+ `error` string explains why, and `gates` is empty. A UI should branch on
8417
+ `available`, not on a non-empty `gates`.
8418
+ """
8419
+ mod = _load_gate_policy_module()
8420
+ if mod is None:
8421
+ return {
8422
+ "schema_version": 1,
8423
+ "available": False,
8424
+ "error": "gate_policy module not found",
8425
+ "status": "unavailable",
8426
+ "ledger": "absent",
8427
+ "gates": [],
8428
+ }
8429
+ try:
8430
+ res = mod.assess(str(_get_loki_dir()))
8431
+ except Exception as e:
8432
+ return {
8433
+ "schema_version": 1,
8434
+ "available": False,
8435
+ "error": f"gate policy assessment failed: {e}",
8436
+ "status": "unavailable",
8437
+ "ledger": "absent",
8438
+ "gates": [],
8439
+ }
8440
+ res["available"] = True
8441
+ return res
8442
+
8443
+
8335
8444
  # =============================================================================
8336
8445
  # Pricing API
8337
8446
  # =============================================================================
@@ -10161,6 +10270,71 @@ async def remove_checklist_waiver(item_id: str):
10161
10270
  # Council Hard Gate Endpoint (Phase 4)
10162
10271
  # =============================================================================
10163
10272
 
10273
+ # Receipts name gates in snake_case; the UI rows are display names. Mapping is
10274
+ # explicit rather than derived (a lower()/replace() heuristic would silently
10275
+ # mis-map "Test Suite" <-> "test_coverage" and "Test Mutation" <->
10276
+ # "mutation_integrity", which are different gates).
10277
+ _RECEIPT_GATE_NAMES = {
10278
+ "static_analysis": "Static Analysis",
10279
+ "test_coverage": "Test Suite",
10280
+ "code_review": "Blind Code Review",
10281
+ "anti_sycophancy": "Anti-Sycophancy",
10282
+ "mock_integrity": "Mock Integrity",
10283
+ "mutation_integrity": "Test Mutation",
10284
+ "doc_coverage": "Documentation Coverage",
10285
+ "magic_debate": "Magic Modules Debate",
10286
+ }
10287
+
10288
+
10289
+ # Receipts say "passed"/"failed"; the UI switches on "pass"/"fail"
10290
+ # (dashboard-ui/components/loki-quality-gates.js:51). Passing the receipt's word
10291
+ # through unchanged matched nothing, so every gate fell to the "pending" default
10292
+ # and the page still read "0 Pass, 0 Fail, 8 Pending" while showing real
10293
+ # timestamps -- half-fixed, and only visible by loading the page.
10294
+ #
10295
+ # An UNRECOGNISED status maps to nothing and the gate stays pending. Guessing
10296
+ # (e.g. treating any unknown word as a pass) is how a future status string would
10297
+ # silently become a green badge.
10298
+ _RECEIPT_GATE_STATUS = {
10299
+ "passed": "pass", "pass": "pass", "ok": "pass",
10300
+ "failed": "fail", "fail": "fail", "blocked": "fail",
10301
+ }
10302
+
10303
+
10304
+ def _receipt_backed_gates():
10305
+ """_DEFAULT_QUALITY_GATES, with results filled in from the newest receipt.
10306
+
10307
+ Fail-open by design: any error returns the plain defaults, so a malformed
10308
+ receipt degrades to "pending" rather than breaking the Quality page.
10309
+
10310
+ Gates absent from the receipt keep status "pending" and get NO last_checked.
10311
+ An absent measurement is never rendered as a result.
10312
+ """
10313
+ gates = [dict(g) for g in _DEFAULT_QUALITY_GATES]
10314
+ try:
10315
+ proofs = sorted((_get_loki_dir() / "proofs").glob("*/proof.json"))
10316
+ if not proofs:
10317
+ return gates
10318
+ receipt = json.loads(proofs[-1].read_text())
10319
+ checked_at = receipt.get("generated_at") or ""
10320
+ by_display = {}
10321
+ for entry in receipt.get("quality_gates", {}).get("gates", []) or []:
10322
+ display = _RECEIPT_GATE_NAMES.get(entry.get("name", ""))
10323
+ status = _RECEIPT_GATE_STATUS.get(str(entry.get("status", "")).lower())
10324
+ if display and status:
10325
+ by_display[display] = status
10326
+ for gate in gates:
10327
+ status = by_display.get(gate.get("name"))
10328
+ if status:
10329
+ gate["status"] = status
10330
+ if checked_at:
10331
+ gate["last_checked"] = checked_at
10332
+ gate["source"] = "receipt"
10333
+ except (OSError, ValueError, KeyError, TypeError, AttributeError):
10334
+ return [dict(g) for g in _DEFAULT_QUALITY_GATES]
10335
+ return gates
10336
+
10337
+
10164
10338
  _DEFAULT_QUALITY_GATES = [
10165
10339
  {"name": "Static Analysis", "description": "CodeQL, ESLint/Pylint, type-checker findings on the diff", "status": "pending"},
10166
10340
  {"name": "Test Suite", "description": "Project test runner pass/fail (red blocks)", "status": "pending"},
@@ -10199,7 +10373,22 @@ async def get_council_gate():
10199
10373
  except (json.JSONDecodeError, IOError):
10200
10374
  data = {"blocked": False, "gates": _DEFAULT_QUALITY_GATES, "error": "Failed to read gate file"}
10201
10375
  else:
10202
- data = {"blocked": False, "gates": _DEFAULT_QUALITY_GATES}
10376
+ # No live gate file. Fall back to the most recent RECEIPT, which already
10377
+ # records per-gate results -- rather than serving eight rows that all
10378
+ # say "Last checked: Never" while .loki/proofs/ holds the real answers.
10379
+ #
10380
+ # That "Never" was not a display bug: /api/council/gate served
10381
+ # _DEFAULT_QUALITY_GATES verbatim, and those entries carry no
10382
+ # last_checked, so the UI honestly rendered "Never" for every gate on a
10383
+ # repo with 9 receipts.
10384
+ #
10385
+ # ONLY gates the receipt actually names are filled in. Measured across
10386
+ # all 9 receipts here, that is 3 of 8 (static_analysis, code_review,
10387
+ # doc_coverage); the other five stay "pending" with no timestamp,
10388
+ # because a gate that never ran must not inherit a neighbour's result.
10389
+ # Filling all eight from a 3-gate receipt is the exact false-green this
10390
+ # codebase exists to prevent.
10391
+ data = {"blocked": False, "gates": _receipt_backed_gates()}
10203
10392
 
10204
10393
  # Verified-completion evidence gate (additive).
10205
10394
  if evidence_file.exists():