loki-mode 9.17.0 → 9.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md 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.17.0
6
+ # Loki Mode v9.17.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.17.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.17.2 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.17.0
1
+ 9.17.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. .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
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
- 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.
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, criteria_declared}. brief truncated to
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():
@@ -91,25 +91,9 @@ def _authorship_line(loki_dir, run_id=None):
91
91
 
92
92
 
93
93
  def _grounding_line(loki_dir):
94
- # The completion route now persists the per-claim result here
95
- # (run.sh:_loki_check_claim_grounding, written at the non-destructive claim
96
- # peek). Absent file means the run never reached a completion claim, which
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
- # Three states, not two. Collapsing everything non-measured to UNKNOWN
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 "