loki-mode 9.17.0 → 9.18.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 +444 -264
- package/autonomy/notify.sh +1 -70
- package/autonomy/queue-consumer.sh +18 -290
- package/autonomy/run.sh +280 -189
- package/autonomy/trigger-server.py +518 -17
- package/autonomy/verify.sh +100 -12
- package/bin/loki +7 -1
- 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/COMPETITIVE-SCORECARD.md +38 -0
- package/docs/COMPETITOR-DEPLOYMENT-MODELS.md +475 -0
- package/docs/DEPLOYMENT.md +542 -0
- package/docs/STALE-STATE-AUDIT.md +174 -0
- package/docs/VERIFICATION-COST.md +46 -166
- 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/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.18.2
|
|
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.18.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
9.
|
|
1
|
+
9.18.2
|
|
@@ -380,29 +380,6 @@ repo = data.get('repo', '')
|
|
|
380
380
|
|
|
381
381
|
labels_str = ', '.join(labels) if labels else ''
|
|
382
382
|
|
|
383
|
-
_ac_text = (title + ' ' + body).lower()
|
|
384
|
-
_ac_rules = [
|
|
385
|
-
(['save', 'persist', 'store', 'databas', 'crud'], 'PERSIST',
|
|
386
|
-
'Data the change writes survives a restart (a real store, not in-memory state).'),
|
|
387
|
-
(['auth', 'login', 'sign in', 'session', 'permission'], 'AUTH',
|
|
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'], 'API',
|
|
390
|
-
'Each affected endpoint returns the documented status codes and is callable without a browser.'),
|
|
391
|
-
(['payment', 'stripe', 'billing', 'invoice', 'subscription'], 'PAY',
|
|
392
|
-
'The payment path runs against provider test mode; no mocked charge stands in for the integration.'),
|
|
393
|
-
(['bug', 'fix', 'regression', 'broken', 'crash', 'error'], 'REPRO',
|
|
394
|
-
'A test reproduces the reported failure and FAILS before the fix, then passes after it.'),
|
|
395
|
-
(['perf', 'slow', 'latency', 'timeout', 'memory leak'], 'PERF',
|
|
396
|
-
'The improvement is measured before and after, and the numbers appear in the change.'),
|
|
397
|
-
(['security', 'vulnerab', 'injection', 'xss', 'csrf'], 'SEC',
|
|
398
|
-
'A test demonstrates the vulnerable behavior is refused after the change.'),
|
|
399
|
-
|
|
400
|
-
]
|
|
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 ''
|
|
405
|
-
|
|
406
383
|
prd = f'''# PRD: {title}
|
|
407
384
|
|
|
408
385
|
**Source:** {provider.replace('_', ' ').title()} Issue [{number}]({url})
|
|
@@ -431,7 +408,6 @@ Based on the issue description, implement the following:
|
|
|
431
408
|
2. Ensure backward compatibility (unless explicitly breaking changes are requested)
|
|
432
409
|
3. Add appropriate tests for new functionality
|
|
433
410
|
4. Update documentation as needed
|
|
434
|
-
{derived_ac}
|
|
435
411
|
|
|
436
412
|
---
|
|
437
413
|
|
|
@@ -186,93 +186,15 @@ 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
|
-
|
|
247
189
|
def main(argv):
|
|
248
190
|
as_json = "--json" in argv
|
|
249
|
-
do_fix = "--fix" in argv
|
|
250
191
|
root = "."
|
|
251
192
|
for a in argv:
|
|
252
193
|
if not a.startswith("-"):
|
|
253
194
|
root = a
|
|
254
195
|
break
|
|
255
196
|
res = assess(root)
|
|
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.")
|
|
197
|
+
print(json.dumps(res, indent=2) if as_json else render_text(res))
|
|
276
198
|
return 0 if res.get("status") == "measured" else 3
|
|
277
199
|
|
|
278
200
|
|
|
@@ -93,92 +93,6 @@ 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
|
-
|
|
182
96
|
|
|
183
97
|
def _git(args, cwd, timeout=30):
|
|
184
98
|
"""Run a git command read-only. Returns (rc, stdout). Never raises.
|
|
@@ -401,9 +315,6 @@ def outcome_for_receipt(proof_path, cwd):
|
|
|
401
315
|
state, reason = resolve_anchor(base_sha, head_sha, files, cwd)
|
|
402
316
|
rec["anchor"] = {"state": state, "reason": reason}
|
|
403
317
|
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"])
|
|
407
318
|
rec["outcome"] = UNKNOWN
|
|
408
319
|
rec["reason"] = ANCHOR_REASONS.get(reason, reason or "not anchored")
|
|
409
320
|
rec["commands"] = [
|
|
@@ -470,15 +381,11 @@ def summarize(records):
|
|
|
470
381
|
# as "no data" instead of "your receipts are not recording a landed sha",
|
|
471
382
|
# which is an actionable defect.
|
|
472
383
|
reasons = {}
|
|
473
|
-
klasses = {"by_design": 0, "historical": 0, "regression": 0, "live": 0}
|
|
474
384
|
for r in records:
|
|
475
385
|
a = r.get("anchor") or {}
|
|
476
386
|
if a.get("state") and a["state"] != "anchored":
|
|
477
387
|
key = a.get("reason") or "unknown"
|
|
478
388
|
reasons[key] = reasons.get(key, 0) + 1
|
|
479
|
-
k = a.get("klass")
|
|
480
|
-
if k in klasses:
|
|
481
|
-
klasses[k] += 1
|
|
482
389
|
|
|
483
390
|
summary = {
|
|
484
391
|
"receipts_total": total,
|
|
@@ -486,12 +393,6 @@ def summarize(records):
|
|
|
486
393
|
"receipts_unknown": len(unknown),
|
|
487
394
|
"reverted": len(reverted),
|
|
488
395
|
"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"],
|
|
495
396
|
}
|
|
496
397
|
if measured:
|
|
497
398
|
summary["change_failure_rate"] = round(len(reverted) / len(measured), 4)
|
|
@@ -541,29 +442,6 @@ def render_text(records, summary, note=None):
|
|
|
541
442
|
out.append(" Why not anchored (a receipt must prove base..head IS the change):")
|
|
542
443
|
for k, n in sorted(reasons.items(), key=lambda kv: -kv[1]):
|
|
543
444
|
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.")
|
|
567
445
|
out.append("")
|
|
568
446
|
cfr = summary.get("change_failure_rate")
|
|
569
447
|
if cfr == UNKNOWN:
|
|
@@ -943,46 +943,12 @@ 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.
|
|
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
|
|
946
|
+
2. The empty tree -- correct for a GREENFIELD run (a repo with no commits
|
|
952
947
|
at start), where "everything that now exists" IS the run's output and
|
|
953
948
|
there is no earlier commit to diff against.
|
|
954
|
-
|
|
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.
|
|
949
|
+
3. Empty string -- let workspace_diff apply its own fallbacks.
|
|
968
950
|
"""
|
|
969
951
|
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
|
|
986
952
|
if not base:
|
|
987
953
|
# Greenfield: no baseline commit existed when the run started.
|
|
988
954
|
base = _empty_tree_sha(target_dir)
|
|
@@ -1024,33 +990,8 @@ def _collect_iterations(loki_dir):
|
|
|
1024
990
|
return {"count": count, "succeeded": n_completed, "failed": n_failed}
|
|
1025
991
|
|
|
1026
992
|
|
|
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
|
-
|
|
1051
993
|
def _collect_spec(loki_dir, target_dir):
|
|
1052
|
-
"""Return spec dict {source, brief
|
|
1053
|
-
600 chars."""
|
|
994
|
+
"""Return spec dict {source, brief}. brief truncated to 600 chars."""
|
|
1054
995
|
prd_path = os.environ.get("PRD_PATH", "").strip()
|
|
1055
996
|
source = ""
|
|
1056
997
|
brief = ""
|
|
@@ -1077,15 +1018,7 @@ def _collect_spec(loki_dir, target_dir):
|
|
|
1077
1018
|
# Full brief here; the <=600 cap is applied AFTER redaction in generate()
|
|
1078
1019
|
# so a secret straddling the cap cannot be sliced into an under-length
|
|
1079
1020
|
# fragment that bypasses the redactor.
|
|
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
|
-
}
|
|
1021
|
+
return {"source": source, "brief": brief}
|
|
1089
1022
|
|
|
1090
1023
|
|
|
1091
1024
|
def _self_version():
|
package/autonomy/lib/verdict.py
CHANGED
|
@@ -91,25 +91,9 @@ def _authorship_line(loki_dir, run_id=None):
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def _grounding_line(loki_dir):
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
# is UNKNOWN -- not a pass, and not a failure.
|
|
98
|
-
d = _read_json(os.path.join(loki_dir, "state", "claim-grounding.json"))
|
|
99
|
-
if not isinstance(d, dict) or d.get("status") != "measured":
|
|
100
|
-
return UNKNOWN, "no completion claim was checked against the diff"
|
|
101
|
-
named = d.get("paths_named") or []
|
|
102
|
-
if not named:
|
|
103
|
-
# Reported by name rather than scored: a claim naming no path is
|
|
104
|
-
# UNGROUNDABLE, which is a different fact from a claim that checked out.
|
|
105
|
-
return UNKNOWN, "the completion claim named no file path, so it cannot be grounded"
|
|
106
|
-
ungrounded = d.get("ungrounded") or []
|
|
107
|
-
if d.get("has_ungrounded_claim"):
|
|
108
|
-
return "finding", (
|
|
109
|
-
f"the completion claim names {len(ungrounded)} path(s) absent from the diff: "
|
|
110
|
-
+ ", ".join(str(p) for p in ungrounded[:3])
|
|
111
|
-
)
|
|
112
|
-
return "measured", f"all {len(named)} path(s) named in the completion claim are in the diff"
|
|
94
|
+
# Grounding is computed per claim at completion time, not stored, so this
|
|
95
|
+
# reports availability rather than inventing a stale result.
|
|
96
|
+
return UNKNOWN, "run `loki verify` to check the completion claim against the diff"
|
|
113
97
|
|
|
114
98
|
|
|
115
99
|
def _model_line(loki_dir):
|
|
@@ -177,12 +161,7 @@ def render_markdown(v):
|
|
|
177
161
|
out.append("| Signal | Status | Detail |")
|
|
178
162
|
out.append("|---|---|---|")
|
|
179
163
|
for r in v["signals"]:
|
|
180
|
-
|
|
181
|
-
# would render a real finding ("the claim names a file absent from the
|
|
182
|
-
# diff") identically to "we could not check" -- the false equivalence
|
|
183
|
-
# this whole module exists to refuse. Anything unrecognised still
|
|
184
|
-
# degrades to UNKNOWN, so an unmeasured signal can never read as a pass.
|
|
185
|
-
status = r["status"] if r["status"] in ("measured", "finding") else UNKNOWN
|
|
164
|
+
status = "measured" if r["status"] == "measured" else "UNKNOWN"
|
|
186
165
|
out.append(f"| {r['signal']} | {status} | {r['detail']} |")
|
|
187
166
|
out.append("")
|
|
188
167
|
out.append("Every line is derived from a file in `.loki/` that you can read "
|