loki-mode 9.16.0 → 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.
- package/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/issue-providers.sh +12 -9
- package/autonomy/lib/agent_readiness.py +79 -1
- package/autonomy/lib/gate_policy.py +166 -0
- package/autonomy/lib/outcome_ledger.py +122 -0
- package/autonomy/lib/proof-generator.py +71 -4
- package/autonomy/loki +83 -12
- package/autonomy/notify.sh +70 -1
- package/autonomy/queue-consumer.sh +290 -18
- package/autonomy/run.sh +11 -1
- package/completions/_loki +1 -0
- package/completions/loki.bash +2 -2
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +168 -1
- package/dashboard/static/index.html +248 -55
- package/docs/QUEUE-OPERATIONS.md +107 -0
- package/docs/VERIFICATION-COST.md +170 -0
- package/loki-ts/dist/loki.js +287 -293
- 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.17.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.17.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.17.0
|
|
@@ -382,23 +382,26 @@ labels_str = ', '.join(labels) if labels else ''
|
|
|
382
382
|
|
|
383
383
|
_ac_text = (title + ' ' + body).lower()
|
|
384
384
|
_ac_rules = [
|
|
385
|
-
(['save', 'persist', 'store', 'databas', 'crud'],
|
|
385
|
+
(['save', 'persist', 'store', 'databas', 'crud'], 'PERSIST',
|
|
386
386
|
'Data the change writes survives a restart (a real store, not in-memory state).'),
|
|
387
|
-
(['auth', 'login', 'sign in', 'session', 'permission'],
|
|
387
|
+
(['auth', 'login', 'sign in', 'session', 'permission'], 'AUTH',
|
|
388
388
|
'The auth path is exercised end to end including the denied case (401/403), not only the happy path.'),
|
|
389
|
-
(['api', 'endpoint', 'rest', 'graphql', 'route'],
|
|
389
|
+
(['api', 'endpoint', 'rest', 'graphql', 'route'], 'API',
|
|
390
390
|
'Each affected endpoint returns the documented status codes and is callable without a browser.'),
|
|
391
|
-
(['payment', 'stripe', 'billing', 'invoice', 'subscription'],
|
|
391
|
+
(['payment', 'stripe', 'billing', 'invoice', 'subscription'], 'PAY',
|
|
392
392
|
'The payment path runs against provider test mode; no mocked charge stands in for the integration.'),
|
|
393
|
-
(['bug', 'fix', 'regression', 'broken', 'crash', 'error'],
|
|
393
|
+
(['bug', 'fix', 'regression', 'broken', 'crash', 'error'], 'REPRO',
|
|
394
394
|
'A test reproduces the reported failure and FAILS before the fix, then passes after it.'),
|
|
395
|
-
(['perf', 'slow', 'latency', 'timeout', 'memory leak'],
|
|
395
|
+
(['perf', 'slow', 'latency', 'timeout', 'memory leak'], 'PERF',
|
|
396
396
|
'The improvement is measured before and after, and the numbers appear in the change.'),
|
|
397
|
-
(['security', 'vulnerab', 'injection', 'xss', 'csrf'],
|
|
397
|
+
(['security', 'vulnerab', 'injection', 'xss', 'csrf'], 'SEC',
|
|
398
398
|
'A test demonstrates the vulnerable behavior is refused after the change.'),
|
|
399
|
+
|
|
399
400
|
]
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
# Stable, content-derived IDs: the axis comes from WHICH rule fired, so adding
|
|
402
|
+
# a rule never renumbers an existing criterion and a receipt can cite one.
|
|
403
|
+
_hits = [(ax, c) for kws, ax, c in _ac_rules if any(k in _ac_text for k in kws)]
|
|
404
|
+
derived_ac = ('\n'.join(f'- AC-{ax}-001: {c}' for ax, c in _hits) + '\n') if _hits else ''
|
|
402
405
|
|
|
403
406
|
prd = f'''# PRD: {title}
|
|
404
407
|
|
|
@@ -186,15 +186,93 @@ def render_text(res):
|
|
|
186
186
|
return "\n".join(out)
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
# What --fix writes for each missing criterion. Only files whose CORRECT content
|
|
190
|
+
# can be derived from the repo itself appear here.
|
|
191
|
+
#
|
|
192
|
+
# test_command and dependency_lock are deliberately absent: guessing a test
|
|
193
|
+
# command writes a line that lies (`npm test` in a repo with no runner exits
|
|
194
|
+
# non-zero forever, and the readiness check would then report "present" for
|
|
195
|
+
# something that does not work), and a lockfile must come from the real package
|
|
196
|
+
# manager or it is worse than none. Those stay REPORTED, never generated.
|
|
197
|
+
#
|
|
198
|
+
# This is the difference between a scorecard and an executable one -- Factory's
|
|
199
|
+
# /readiness-report -> /readiness-fix -- without the failure mode where the fix
|
|
200
|
+
# makes the score green while the underlying capability is still missing.
|
|
201
|
+
FIXABLE = {
|
|
202
|
+
"gitignore": (".gitignore", "node_modules/\n__pycache__/\n.env\n.venv/\ndist/\n*.log\n"),
|
|
203
|
+
"readme": ("README.md", None), # content derived below
|
|
204
|
+
"agent_brief": ("AGENTS.md", None), # content derived below
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _fix(root, res):
|
|
209
|
+
"""Create the missing files whose content can be derived honestly.
|
|
210
|
+
|
|
211
|
+
Returns (written, skipped) where skipped names criteria that a generated
|
|
212
|
+
file could not honestly satisfy.
|
|
213
|
+
"""
|
|
214
|
+
written, skipped = [], []
|
|
215
|
+
project = os.path.basename(os.path.abspath(root)) or "this project"
|
|
216
|
+
# assess() reports `missing` as a list of criterion IDs (strings), not the
|
|
217
|
+
# full check dicts. Tolerate both so a later shape change does not silently
|
|
218
|
+
# fix nothing.
|
|
219
|
+
for entry in res.get("missing", []):
|
|
220
|
+
cid = entry["id"] if isinstance(entry, dict) else entry
|
|
221
|
+
if cid not in FIXABLE:
|
|
222
|
+
skipped.append((cid, "must come from the real toolchain, not a guess"))
|
|
223
|
+
continue
|
|
224
|
+
name, body = FIXABLE[cid]
|
|
225
|
+
path = os.path.join(root, name)
|
|
226
|
+
if os.path.exists(path):
|
|
227
|
+
continue
|
|
228
|
+
if cid == "readme":
|
|
229
|
+
body = (f"# {project}\n\n"
|
|
230
|
+
"## What this is\n\n_TODO: one paragraph._\n\n"
|
|
231
|
+
"## Run it\n\n```sh\n# TODO: the command that starts this project\n```\n\n"
|
|
232
|
+
"## Test it\n\n```sh\n# TODO: the command that runs the tests\n```\n")
|
|
233
|
+
elif cid == "agent_brief":
|
|
234
|
+
body = (f"# AGENTS.md\n\nBriefing for coding agents working in {project}.\n\n"
|
|
235
|
+
"## Commands\n\n- Build: _TODO_\n- Test: _TODO_\n- Lint: _TODO_\n\n"
|
|
236
|
+
"## Conventions\n\n_TODO: what a reviewer would flag._\n\n"
|
|
237
|
+
"## Do not\n\n_TODO: the things that break this repo._\n")
|
|
238
|
+
try:
|
|
239
|
+
with open(path, "w") as fh:
|
|
240
|
+
fh.write(body)
|
|
241
|
+
written.append(name)
|
|
242
|
+
except OSError as exc:
|
|
243
|
+
skipped.append((cid, f"could not write {name}: {exc}"))
|
|
244
|
+
return written, skipped
|
|
245
|
+
|
|
246
|
+
|
|
189
247
|
def main(argv):
|
|
190
248
|
as_json = "--json" in argv
|
|
249
|
+
do_fix = "--fix" in argv
|
|
191
250
|
root = "."
|
|
192
251
|
for a in argv:
|
|
193
252
|
if not a.startswith("-"):
|
|
194
253
|
root = a
|
|
195
254
|
break
|
|
196
255
|
res = assess(root)
|
|
197
|
-
|
|
256
|
+
|
|
257
|
+
if do_fix and res.get("status") == "measured":
|
|
258
|
+
written, skipped = _fix(root, res)
|
|
259
|
+
res = assess(root) # re-measure: report what is true AFTER the fix
|
|
260
|
+
res["fix"] = {"written": written,
|
|
261
|
+
"skipped": [{"id": i, "reason": r} for i, r in skipped]}
|
|
262
|
+
|
|
263
|
+
if as_json:
|
|
264
|
+
print(json.dumps(res, indent=2))
|
|
265
|
+
else:
|
|
266
|
+
print(render_text(res))
|
|
267
|
+
if do_fix:
|
|
268
|
+
fx = res.get("fix", {})
|
|
269
|
+
print("")
|
|
270
|
+
for name in fx.get("written", []):
|
|
271
|
+
print(f" wrote {name} (a stub -- fill in the TODOs)")
|
|
272
|
+
for s in fx.get("skipped", []):
|
|
273
|
+
print(f" skipped {s['id']}: {s['reason']}")
|
|
274
|
+
if not fx.get("written") and not fx.get("skipped"):
|
|
275
|
+
print(" nothing to fix.")
|
|
198
276
|
return 0 if res.get("status") == "measured" else 3
|
|
199
277
|
|
|
200
278
|
|
|
@@ -0,0 +1,166 @@
|
|
|
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:]))
|
|
@@ -93,6 +93,92 @@ ANCHOR_REASONS = {
|
|
|
93
93
|
"diff_range_mismatch": "the base..head diff does not match the receipt's file set",
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
# HISTORICAL vs LIVE: the distinction that stops an old receipt from reading as
|
|
97
|
+
# a regression.
|
|
98
|
+
#
|
|
99
|
+
# `ANCHORED 0 of 9` is alarming until you know WHY. Two very different things
|
|
100
|
+
# produce it, and a flat count of reasons cannot tell them apart:
|
|
101
|
+
#
|
|
102
|
+
# FROZEN The receipt file itself lacks the anchor. base_sha was written as
|
|
103
|
+
# "" and that JSON is on disk forever. No future fix can anchor it,
|
|
104
|
+
# and rewriting a receipt to make a metric look better is the exact
|
|
105
|
+
# dishonesty this module exists to refuse. These are HISTORY.
|
|
106
|
+
# LIVE The receipt records real shas; only the ENVIRONMENT cannot resolve
|
|
107
|
+
# them right now -- a shallow clone, an unmerged branch, or a file
|
|
108
|
+
# set that still has uncommitted edits. These can anchor later, with
|
|
109
|
+
# no change to the receipt.
|
|
110
|
+
#
|
|
111
|
+
# Measured on this repo: all 9 receipts are frozen (8 base_sha_empty, 1
|
|
112
|
+
# greenfield_no_baseline) and were generated 2026-07-27 and 2026-07-31, BEFORE
|
|
113
|
+
# proof-generator learned to read .loki/state/start-sha (commit 99ce689d,
|
|
114
|
+
# 2026-08-07). So the zero is fully explained by history.
|
|
115
|
+
# Frozen because the GENERATOR failed to record something it should have. These
|
|
116
|
+
# are the only two where recency is meaningful: before the fix they are history,
|
|
117
|
+
# after it they are a live bug in proof-generator.
|
|
118
|
+
FROZEN_GENERATOR_REASONS = frozenset({
|
|
119
|
+
"head_sha_empty",
|
|
120
|
+
"base_sha_empty",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# Frozen BY DESIGN, and correct at any date. A genuinely greenfield repo has no
|
|
124
|
+
# earlier commit to diff against, and a receipt written over uncommitted work
|
|
125
|
+
# honestly has base == head. Neither can ever anchor, and neither is a defect --
|
|
126
|
+
# so recency must NOT be applied to them. Calling a correct greenfield receipt a
|
|
127
|
+
# regression would be a false alarm on the signal added to prevent false alarms.
|
|
128
|
+
FROZEN_BY_DESIGN_REASONS = frozenset({
|
|
129
|
+
"greenfield_no_baseline",
|
|
130
|
+
"change_not_committed",
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
FROZEN_REASONS = FROZEN_GENERATOR_REASONS | FROZEN_BY_DESIGN_REASONS
|
|
134
|
+
|
|
135
|
+
# The two sets must stay disjoint. If a reason appeared in both, classification
|
|
136
|
+
# would depend on which branch is checked first -- and a by-design case that
|
|
137
|
+
# drifted into the generator set would start alarming as a regression the moment
|
|
138
|
+
# someone reordered the checks. Assert the invariant rather than trusting order.
|
|
139
|
+
assert not (FROZEN_GENERATOR_REASONS & FROZEN_BY_DESIGN_REASONS), \
|
|
140
|
+
"a reason cannot be both a generator failure and correct by design"
|
|
141
|
+
|
|
142
|
+
# The commit that taught proof-generator to resolve base_sha from
|
|
143
|
+
# .loki/state/start-sha when the env var is absent. A receipt generated at or
|
|
144
|
+
# after this instant should carry a real baseline, so a FROZEN reason on one is
|
|
145
|
+
# NOT history -- it is a live regression in the generator.
|
|
146
|
+
#
|
|
147
|
+
# Recency is the discriminator because it is the only one the receipts actually
|
|
148
|
+
# support: they carry generated_at (verified on all 9), and loki_version tracks
|
|
149
|
+
# releases rather than this fix. A frozen-vs-live split ALONE would file a newly
|
|
150
|
+
# broken receipt under history, which is the green-wash this guards against.
|
|
151
|
+
BASE_SHA_FIX_UTC = "2026-08-07T13:39:32Z" # 99ce689d, committed 09:39:32 -04:00
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def classify_unanchored(reason, generated_at):
|
|
155
|
+
"""Bucket an unanchored receipt: historical, regression, or live.
|
|
156
|
+
|
|
157
|
+
Returns one of:
|
|
158
|
+
"by_design" -- unanchorable and CORRECT: a greenfield run with no earlier
|
|
159
|
+
commit, or a receipt over uncommitted work. Never a defect,
|
|
160
|
+
at any date, so recency is not applied.
|
|
161
|
+
"historical" -- the generator failed to record an anchor, in a receipt
|
|
162
|
+
written BEFORE the fix. History, and never anchorable now.
|
|
163
|
+
"regression" -- the same generator failure AFTER the fix. The anchor is
|
|
164
|
+
being dropped again. This is the only bucket that alarms.
|
|
165
|
+
"live" -- the receipt is fine; the environment cannot resolve it yet.
|
|
166
|
+
"""
|
|
167
|
+
if reason in FROZEN_BY_DESIGN_REASONS:
|
|
168
|
+
return "by_design"
|
|
169
|
+
if reason not in FROZEN_GENERATOR_REASONS:
|
|
170
|
+
return "live"
|
|
171
|
+
# No timestamp means we cannot place it relative to the fix. Refuse to call
|
|
172
|
+
# it history, because that is the direction that hides a regression.
|
|
173
|
+
#
|
|
174
|
+
# ponytail: lexicographic compare, correct only for the "...Z" ISO form the
|
|
175
|
+
# generator writes (verified on all 9 receipts). An offset-form timestamp
|
|
176
|
+
# would sort below the constant and read as historical -- the hiding
|
|
177
|
+
# direction. Parse properly only if a generator ever emits offsets.
|
|
178
|
+
if not generated_at:
|
|
179
|
+
return "regression"
|
|
180
|
+
return "historical" if generated_at < BASE_SHA_FIX_UTC else "regression"
|
|
181
|
+
|
|
96
182
|
|
|
97
183
|
def _git(args, cwd, timeout=30):
|
|
98
184
|
"""Run a git command read-only. Returns (rc, stdout). Never raises.
|
|
@@ -315,6 +401,9 @@ def outcome_for_receipt(proof_path, cwd):
|
|
|
315
401
|
state, reason = resolve_anchor(base_sha, head_sha, files, cwd)
|
|
316
402
|
rec["anchor"] = {"state": state, "reason": reason}
|
|
317
403
|
if state != "anchored":
|
|
404
|
+
# An old receipt is not a regression. Classify so a reader can tell a
|
|
405
|
+
# frozen pre-fix receipt from a generator that started dropping anchors.
|
|
406
|
+
rec["anchor"]["klass"] = classify_unanchored(reason, rec["generated_at"])
|
|
318
407
|
rec["outcome"] = UNKNOWN
|
|
319
408
|
rec["reason"] = ANCHOR_REASONS.get(reason, reason or "not anchored")
|
|
320
409
|
rec["commands"] = [
|
|
@@ -381,11 +470,15 @@ def summarize(records):
|
|
|
381
470
|
# as "no data" instead of "your receipts are not recording a landed sha",
|
|
382
471
|
# which is an actionable defect.
|
|
383
472
|
reasons = {}
|
|
473
|
+
klasses = {"by_design": 0, "historical": 0, "regression": 0, "live": 0}
|
|
384
474
|
for r in records:
|
|
385
475
|
a = r.get("anchor") or {}
|
|
386
476
|
if a.get("state") and a["state"] != "anchored":
|
|
387
477
|
key = a.get("reason") or "unknown"
|
|
388
478
|
reasons[key] = reasons.get(key, 0) + 1
|
|
479
|
+
k = a.get("klass")
|
|
480
|
+
if k in klasses:
|
|
481
|
+
klasses[k] += 1
|
|
389
482
|
|
|
390
483
|
summary = {
|
|
391
484
|
"receipts_total": total,
|
|
@@ -393,6 +486,12 @@ def summarize(records):
|
|
|
393
486
|
"receipts_unknown": len(unknown),
|
|
394
487
|
"reverted": len(reverted),
|
|
395
488
|
"unanchored_reasons": reasons,
|
|
489
|
+
# The count that turns an alarming zero into an explained one. A
|
|
490
|
+
# regression here is the only bucket that warrants action.
|
|
491
|
+
"unanchored_by_design": klasses["by_design"],
|
|
492
|
+
"unanchored_historical": klasses["historical"],
|
|
493
|
+
"unanchored_regression": klasses["regression"],
|
|
494
|
+
"unanchored_live": klasses["live"],
|
|
396
495
|
}
|
|
397
496
|
if measured:
|
|
398
497
|
summary["change_failure_rate"] = round(len(reverted) / len(measured), 4)
|
|
@@ -442,6 +541,29 @@ def render_text(records, summary, note=None):
|
|
|
442
541
|
out.append(" Why not anchored (a receipt must prove base..head IS the change):")
|
|
443
542
|
for k, n in sorted(reasons.items(), key=lambda kv: -kv[1]):
|
|
444
543
|
out.append(f" {k:24} {n:3} {ANCHOR_REASONS.get(k, '')}")
|
|
544
|
+
|
|
545
|
+
# Without this, ANCHORED 0 of N reads as a regression when it is history.
|
|
546
|
+
hist = summary.get("unanchored_historical", 0)
|
|
547
|
+
regr = summary.get("unanchored_regression", 0)
|
|
548
|
+
live = summary.get("unanchored_live", 0)
|
|
549
|
+
design = summary.get("unanchored_by_design", 0)
|
|
550
|
+
if hist or regr or live or design:
|
|
551
|
+
out.append("")
|
|
552
|
+
if design:
|
|
553
|
+
out.append(f" {design} by design: a greenfield run or uncommitted"
|
|
554
|
+
f" work has no baseline to diff against. Correct, not a"
|
|
555
|
+
f" defect, and never anchorable.")
|
|
556
|
+
if hist:
|
|
557
|
+
out.append(f" {hist} historical: written before the base_sha fix"
|
|
558
|
+
f" ({BASE_SHA_FIX_UTC[:10]}); the receipt itself has no"
|
|
559
|
+
f" baseline, so these can never anchor. Not a defect.")
|
|
560
|
+
if live:
|
|
561
|
+
out.append(f" {live} live: the receipt records real shas; this"
|
|
562
|
+
f" clone cannot resolve them yet. May anchor later.")
|
|
563
|
+
if regr:
|
|
564
|
+
out.append(f" {regr} REGRESSION: written AFTER the fix and still"
|
|
565
|
+
f" missing a baseline. The generator is dropping the"
|
|
566
|
+
f" anchor -- this one is a defect.")
|
|
445
567
|
out.append("")
|
|
446
568
|
cfr = summary.get("change_failure_rate")
|
|
447
569
|
if cfr == UNKNOWN:
|
|
@@ -943,12 +943,46 @@ def _git_diffstat(target_dir, include_diffs):
|
|
|
943
943
|
|
|
944
944
|
Order of preference:
|
|
945
945
|
1. _LOKI_RUN_START_SHA -- the run's own baseline (run.sh exports it).
|
|
946
|
-
2.
|
|
946
|
+
2. .loki/state/start-sha -- the SAME baseline, persisted to disk. The env
|
|
947
|
+
var is only exported inside run_autonomous (run.sh:21690), so a receipt
|
|
948
|
+
generated outside that scope -- `loki proof` run by hand, a resumed
|
|
949
|
+
run, a receipt written after the runner exited -- saw an empty env var
|
|
950
|
+
and fell straight through to the empty tree.
|
|
951
|
+
3. The empty tree -- correct for a GREENFIELD run (a repo with no commits
|
|
947
952
|
at start), where "everything that now exists" IS the run's output and
|
|
948
953
|
there is no earlier commit to diff against.
|
|
949
|
-
|
|
954
|
+
4. Empty string -- let workspace_diff apply its own fallbacks.
|
|
955
|
+
|
|
956
|
+
WHY STEP 2 EXISTS. Without it every receipt on this repo recorded
|
|
957
|
+
base_sha="" -- measured: 9 of 9 receipts in .loki/proofs/, and the dashboard
|
|
958
|
+
correctly reported "9 receipts, 0 verified". An empty base is unanchorable,
|
|
959
|
+
so outcome_ledger.resolve_anchor() returns unanchored/base_sha_empty and NO
|
|
960
|
+
receipt can be verified. The persisted file already existed and both other
|
|
961
|
+
consumers already read it (run.sh:7779, completion-council.sh:1834); this
|
|
962
|
+
reader was the only one that did not, so it silently lost the anchor.
|
|
963
|
+
|
|
964
|
+
The greenfield fallback is deliberately kept BELOW the file: falling back to
|
|
965
|
+
the empty tree when a real baseline exists would attest to "everything in
|
|
966
|
+
the repo" as this run's output, which is a much worse lie than an empty
|
|
967
|
+
base_sha. Order matters more than the addition.
|
|
950
968
|
"""
|
|
951
969
|
base = os.environ.get("_LOKI_RUN_START_SHA", "").strip()
|
|
970
|
+
if not base:
|
|
971
|
+
# Same baseline, read from disk. Mirrors run.sh:7779 and
|
|
972
|
+
# completion-council.sh:1834 rather than inventing a third convention.
|
|
973
|
+
try:
|
|
974
|
+
with open(os.path.join(target_dir, ".loki", "state", "start-sha")) as fh:
|
|
975
|
+
_persisted = fh.read().strip()
|
|
976
|
+
# Only trust it if it names a commit that actually exists HERE. A
|
|
977
|
+
# stale or foreign SHA would produce a diff against nothing and an
|
|
978
|
+
# integrity hash nobody can recompute.
|
|
979
|
+
if _persisted and subprocess.run(
|
|
980
|
+
["git", "cat-file", "-e", _persisted + "^{commit}"],
|
|
981
|
+
cwd=target_dir, capture_output=True, timeout=10,
|
|
982
|
+
).returncode == 0:
|
|
983
|
+
base = _persisted
|
|
984
|
+
except (OSError, subprocess.SubprocessError, ValueError):
|
|
985
|
+
pass
|
|
952
986
|
if not base:
|
|
953
987
|
# Greenfield: no baseline commit existed when the run started.
|
|
954
988
|
base = _empty_tree_sha(target_dir)
|
|
@@ -990,8 +1024,33 @@ def _collect_iterations(loki_dir):
|
|
|
990
1024
|
return {"count": count, "succeeded": n_completed, "failed": n_failed}
|
|
991
1025
|
|
|
992
1026
|
|
|
1027
|
+
# Acceptance-criterion ids as minted at intake ("- AC-<AXIS>-NNN: <text>", see
|
|
1028
|
+
# _brief_acceptance_criteria in autonomy/loki). Anchored and strict on purpose:
|
|
1029
|
+
# a loose pattern would count prose that merely mentions an id, and the whole
|
|
1030
|
+
# value of the id is that a citation points at exactly one criterion.
|
|
1031
|
+
_AC_ID_RE = re.compile(r"^- (AC-[A-Z]+-[0-9]{3}): ", re.MULTILINE)
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def _spec_criteria_declared(text):
|
|
1035
|
+
"""Return the acceptance-criterion ids the spec DECLARES, in spec order.
|
|
1036
|
+
|
|
1037
|
+
DECLARED, NOT SATISFIED. This records which criteria exist in the spec and
|
|
1038
|
+
nothing more -- no check runs here, and no field in the receipt asserts that
|
|
1039
|
+
any of these was met. Naming it criteria_met would be a lie we cannot back.
|
|
1040
|
+
|
|
1041
|
+
Empty list when the spec declares none (an older PRD, a hand-written spec,
|
|
1042
|
+
or a run with no spec file at all). Never invented, never a placeholder.
|
|
1043
|
+
"""
|
|
1044
|
+
if not text:
|
|
1045
|
+
return []
|
|
1046
|
+
# Deduped: an id is a citation target, so each must resolve to one criterion.
|
|
1047
|
+
# A repeated id is a spec bug; counting it twice would not make it citable.
|
|
1048
|
+
return list(dict.fromkeys(_AC_ID_RE.findall(text)))
|
|
1049
|
+
|
|
1050
|
+
|
|
993
1051
|
def _collect_spec(loki_dir, target_dir):
|
|
994
|
-
"""Return spec dict {source, brief}. brief truncated to
|
|
1052
|
+
"""Return spec dict {source, brief, criteria_declared}. brief truncated to
|
|
1053
|
+
600 chars."""
|
|
995
1054
|
prd_path = os.environ.get("PRD_PATH", "").strip()
|
|
996
1055
|
source = ""
|
|
997
1056
|
brief = ""
|
|
@@ -1018,7 +1077,15 @@ def _collect_spec(loki_dir, target_dir):
|
|
|
1018
1077
|
# Full brief here; the <=600 cap is applied AFTER redaction in generate()
|
|
1019
1078
|
# so a secret straddling the cap cannot be sliced into an under-length
|
|
1020
1079
|
# fragment that bypasses the redactor.
|
|
1021
|
-
|
|
1080
|
+
#
|
|
1081
|
+
# Criteria are parsed from the FULL spec text, not from the 600-char display
|
|
1082
|
+
# cap: a PRD's acceptance-criteria block sits well past char 600, so parsing
|
|
1083
|
+
# the capped brief would silently drop most of them.
|
|
1084
|
+
return {
|
|
1085
|
+
"source": source,
|
|
1086
|
+
"brief": brief,
|
|
1087
|
+
"criteria_declared": _spec_criteria_declared(brief),
|
|
1088
|
+
}
|
|
1022
1089
|
|
|
1023
1090
|
|
|
1024
1091
|
def _self_version():
|