loki-mode 10.5.17 → 10.5.20

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 (126) hide show
  1. package/README.md +877 -30
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/autonomy/lib/proof-generator.py +251 -0
  5. package/autonomy/lib/proof-verify.py +1 -0
  6. package/autonomy/loki +52 -6
  7. package/autonomy/run.sh +35 -5
  8. package/autonomy/verify.sh +1 -1
  9. package/dashboard/__init__.py +1 -1
  10. package/dashboard/api_start.py +463 -0
  11. package/dashboard/server.py +31 -0
  12. package/dashboard/static/start.html +100 -0
  13. package/docs/COMPARISON.md +6 -19
  14. package/docs/COMPETITIVE-ANALYSIS.md +6 -41
  15. package/docs/CRASH-REPORTING-PLAN.md +1 -1
  16. package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +1 -1
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/PRIVACY.md +1 -1
  19. package/docs/R8-SHAREABLE-TEAM-ASSETS-PLAN.md +1 -1
  20. package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
  21. package/docs/architecture/STATE-MACHINES.md +18 -27
  22. package/docs/architecture/bmad-integration-validation.md +2 -2
  23. package/docs/audit-logging.md +12 -86
  24. package/docs/authentication.md +4 -4
  25. package/docs/authorization.md +15 -47
  26. package/docs/auto-claude-comparison.md +1 -33
  27. package/docs/certification/01-core-concepts/lab.md +3 -3
  28. package/docs/certification/01-core-concepts/lesson.md +11 -22
  29. package/docs/certification/01-core-concepts/quiz.md +7 -7
  30. package/docs/certification/02-enterprise-features/lab.md +0 -2
  31. package/docs/certification/02-enterprise-features/lesson.md +0 -10
  32. package/docs/certification/04-production-deployment/lab.md +1 -1
  33. package/docs/certification/05-troubleshooting/lab.md +2 -2
  34. package/docs/certification/README.md +1 -1
  35. package/docs/certification/answer-key.md +37 -20
  36. package/docs/certification/certification-exam.md +7 -7
  37. package/docs/competitive/emergence-others-analysis.md +3 -3
  38. package/docs/competitive/replit-lovable-analysis.md +2 -2
  39. package/docs/cursor-comparison.md +2 -2
  40. package/docs/dashboard-guide.md +14 -2
  41. package/docs/dev/architecture-reference.md +2 -2
  42. package/docs/dev/project-structure.md +1 -1
  43. package/docs/enterprise/architecture.md +11 -8
  44. package/docs/enterprise/integration-cookbook.md +23 -14
  45. package/docs/enterprise/migration.md +5 -9
  46. package/docs/enterprise/performance.md +3 -6
  47. package/docs/enterprise/sdk-guide.md +1 -1
  48. package/docs/enterprise/security.md +8 -17
  49. package/docs/exit-codes.md +14 -0
  50. package/docs/git-workflow.md +4 -62
  51. package/docs/network-security.md +3 -13
  52. package/docs/plans/ULTRAPLAN-FORGE-BAAS.md +1 -1
  53. package/docs/prd-purple-lab-platform-v2.md +1 -1
  54. package/docs/prd-purple-lab-platform.md +1 -3
  55. package/docs/research-2026-07/GAP-ANALYSIS-BACKLOG.md +1 -1
  56. package/docs/show-hn-post.md +3 -4
  57. package/docs/siem-integration.md +8 -10
  58. package/docs/test-scenarios/edge-cases.md +2 -2
  59. package/docs/test-scenarios/enterprise-scenarios.md +4 -4
  60. package/docs/v10/BOARD.md +7 -5
  61. package/docs/v10/GUIDE.md +5 -3
  62. package/docs/v10/METRICS.md +36 -0
  63. package/docs/v10/PROGRESS.md +42 -0
  64. package/docs/walkthrough/README.md +1 -7
  65. package/loki-ts/dist/loki.js +370 -370
  66. package/mcp/__init__.py +1 -1
  67. package/package.json +1 -1
  68. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  69. package/web-app/dist/assets/{AdminPage-DxGZSpFg.js → AdminPage-HjtApbcL.js} +1 -1
  70. package/web-app/dist/assets/{Avatar-BZEp23D8.js → Avatar-BcwLENsU.js} +1 -1
  71. package/web-app/dist/assets/{Badge-DnDwqDLu.js → Badge-DjJhHJj0.js} +1 -1
  72. package/web-app/dist/assets/{Button-C9KDP4c1.js → Button-CSMstrCz.js} +1 -1
  73. package/web-app/dist/assets/{CockpitPage-DRlh3LV2.js → CockpitPage-BHihN-aq.js} +1 -1
  74. package/web-app/dist/assets/{ComparePage-C_gE45dQ.js → ComparePage-_6JtexDp.js} +1 -1
  75. package/web-app/dist/assets/{ErrorBoundary-CL-J9-Cs.js → ErrorBoundary-Cx2fnw3q.js} +1 -1
  76. package/web-app/dist/assets/{EvidenceReceiptPanel-DABd176Y.js → EvidenceReceiptPanel-DcUQAnju.js} +1 -1
  77. package/web-app/dist/assets/{GitHubIssuesPanel-C4lYbm6O.js → GitHubIssuesPanel-1dYpQ5Rx.js} +1 -1
  78. package/web-app/dist/assets/{GitHubPRsPanel-CgiUixwo.js → GitHubPRsPanel-BPdDD1kX.js} +1 -1
  79. package/web-app/dist/assets/{HomePage-DC99RIrz.js → HomePage-JfmwLGnQ.js} +2 -2
  80. package/web-app/dist/assets/{LoginPage-Bd72QWNk.js → LoginPage-7SnMHzPc.js} +1 -1
  81. package/web-app/dist/assets/{MagicPage-CBouFs2K.js → MagicPage-C0IkoOzD.js} +1 -1
  82. package/web-app/dist/assets/{MetricsPage-c9MyDcBp.js → MetricsPage-BJOZ0L3F.js} +1 -1
  83. package/web-app/dist/assets/{NotFoundPage-ZPhiebiW.js → NotFoundPage-S49UjLyW.js} +1 -1
  84. package/web-app/dist/assets/{ProjectPage-DKwrALUb.js → ProjectPage-Dj_nv2kT.js} +3 -3
  85. package/web-app/dist/assets/{ProjectsPage-X1uZKZC_.js → ProjectsPage-C7thLjOm.js} +1 -1
  86. package/web-app/dist/assets/{SettingsPage-CiZKZRto.js → SettingsPage-DEcJyfsU.js} +1 -1
  87. package/web-app/dist/assets/{ShowcasePage-k_imKfeh.js → ShowcasePage-B5PshXNg.js} +1 -1
  88. package/web-app/dist/assets/{SystemSettingsPage-Wx9ESLll.js → SystemSettingsPage-Cnp4BkN7.js} +1 -1
  89. package/web-app/dist/assets/{TeamsPage-B7upn-I7.js → TeamsPage-1ImpgMzs.js} +1 -1
  90. package/web-app/dist/assets/{TemplatesPage-oJf_VKgh.js → TemplatesPage-BOYKrPX8.js} +1 -1
  91. package/web-app/dist/assets/{TerminalOutput-BdfPlgLI.js → TerminalOutput-LPxzwfwm.js} +1 -1
  92. package/web-app/dist/assets/{activity-BcZTsgWH.js → activity-Hl7iDpX_.js} +1 -1
  93. package/web-app/dist/assets/{bell-JNEkjWQ2.js → bell-DrBfjAq4.js} +1 -1
  94. package/web-app/dist/assets/{bot-DMAJobP_.js → bot-6SoklKaZ.js} +1 -1
  95. package/web-app/dist/assets/{check-DPTry-z2.js → check-DhHuZH8k.js} +1 -1
  96. package/web-app/dist/assets/{chevron-left-DSqOEtTV.js → chevron-left-BUERGaLO.js} +1 -1
  97. package/web-app/dist/assets/{circle-alert-BbL8Vysv.js → circle-alert-D7UOw5bc.js} +1 -1
  98. package/web-app/dist/assets/{clock-BHEr5lGW.js → clock-t3v_i7fP.js} +1 -1
  99. package/web-app/dist/assets/{cloud-C48PunYo.js → cloud-CP3eLZts.js} +1 -1
  100. package/web-app/dist/assets/{code-xml-Boa7kRqT.js → code-xml-DU-CmwVx.js} +1 -1
  101. package/web-app/dist/assets/{database-Eu35Gv7z.js → database-J27fE_Ot.js} +1 -1
  102. package/web-app/dist/assets/{dollar-sign-BO9fCnHW.js → dollar-sign-BdU_HR9C.js} +1 -1
  103. package/web-app/dist/assets/{file-code-corner-DHsSDIL_.js → file-code-corner-Cu4A1aIH.js} +1 -1
  104. package/web-app/dist/assets/{file-plus-BYiQLe5l.js → file-plus-BTli76r5.js} +1 -1
  105. package/web-app/dist/assets/{globe-BPZ5sQI2.js → globe-Bz3m1c_-.js} +1 -1
  106. package/web-app/dist/assets/{hammer-DlOLTsH3.js → hammer-HNN6jr92.js} +1 -1
  107. package/web-app/dist/assets/{index-BrgkuqSA.js → index-CgK4xqUy.js} +3 -3
  108. package/web-app/dist/assets/{layers-DMNdn1GR.js → layers-DhuXUuM6.js} +1 -1
  109. package/web-app/dist/assets/{loader-circle-_e9fypHL.js → loader-circle-CeBjdh2y.js} +1 -1
  110. package/web-app/dist/assets/{lock-D_elxMZb.js → lock-DjSqmIVZ.js} +1 -1
  111. package/web-app/dist/assets/{package-Bw-7g3JI.js → package-NL0AdpIi.js} +1 -1
  112. package/web-app/dist/assets/{plus-CZeq0pgJ.js → plus-Lbz3hom-.js} +1 -1
  113. package/web-app/dist/assets/{refresh-cw-D0WMyffs.js → refresh-cw-uKARwuER.js} +1 -1
  114. package/web-app/dist/assets/{rotate-ccw-CcKyNwXG.js → rotate-ccw-ByA6XJq1.js} +1 -1
  115. package/web-app/dist/assets/{scroll-text-C0L9Vqpm.js → scroll-text-BV8Blxig.js} +1 -1
  116. package/web-app/dist/assets/{server-SgwFv6cd.js → server-Cy3R5hde.js} +1 -1
  117. package/web-app/dist/assets/{shield-alert-D1tESlfI.js → shield-alert-rE_xXx7r.js} +1 -1
  118. package/web-app/dist/assets/{thumbs-up-bUtUA15O.js → thumbs-up-CNMITZHD.js} +1 -1
  119. package/web-app/dist/assets/{trash-2-C9Nv1kKk.js → trash-2-BP3Rr5SU.js} +1 -1
  120. package/web-app/dist/assets/{trending-down-Bh107AJe.js → trending-down-BE_8lCjD.js} +1 -1
  121. package/web-app/dist/assets/{trending-up-25BqhLVq.js → trending-up-CF6E9-9a.js} +1 -1
  122. package/web-app/dist/assets/{upload-DDYtMbN5.js → upload-D-Ui4N6K.js} +1 -1
  123. package/web-app/dist/assets/{usePolling-BSDT0il_.js → usePolling-DIVX97OV.js} +1 -1
  124. package/web-app/dist/assets/{user-CKpUEUhK.js → user-7rTiTfDj.js} +1 -1
  125. package/web-app/dist/index.html +1 -1
  126. package/docs/README-FULL.md +0 -977
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: An autonomous software factory that knows what it is supposed to deliver, and proves it did. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v10.5.17
6
+ # Loki Mode v10.5.20
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -458,7 +458,7 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
458
458
  |---------|--------|-------|
459
459
  | Bun runtime (Phase 2+) | TBD | Migrate write-path commands; tracked on `feat/bun-migration` |
460
460
  | Managed Agents multiagent path | TBD | `LOKI_EXPERIMENTAL_MANAGED_*` flags -- RESEARCH PREVIEW, not on live API |
461
- | Benchmarks (HumanEval, SWE-bench) | TBD | Runner scripts and datasets exist in `benchmarks/`; no published results |
461
+ | Benchmarks | TBD | Runner scripts and datasets exist in `benchmarks/`; no published results |
462
462
  | `loki run` removal | next major | Currently a deprecated alias for `loki start` |
463
463
 
464
464
  ## Deprecated
@@ -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
- **v10.5.17 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v10.5.20 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 10.5.17
1
+ 10.5.20
@@ -1115,6 +1115,230 @@ def _empty_tree_sha(repo_dir):
1115
1115
  return out.stdout.strip() if out.returncode == 0 else ""
1116
1116
 
1117
1117
 
1118
+ # A-118 / D47: tests_integrity. Did the run WEAKEN the tests that vouch for it?
1119
+ # CFG_ALWAYS is copied from loki-ts/src/engine10/stages/verify.ts (A-115); keep in sync.
1120
+ # The comparison is base vs the final WORKING TREE plus untracked files (like
1121
+ # collect_workspace_diff), so uncommitted work is covered.
1122
+ _TI_CFG_ALWAYS = re.compile(r"(^|/)(conftest\.py|\.?pytest\.(?:ini|toml)|tox\.ini|jest\.config\.[\w.]+|vitest\.config\.[\w.]+|\.mocharc[\w.]*)$")
1123
+ _TI_CFG_SHARED = re.compile(r"(^|/)(setup\.cfg|pyproject\.toml|package\.json)$")
1124
+ _TI_TEST_FILE = re.compile(
1125
+ r"(^|/)(tests?|__tests__|spec|specs)/|\.(test|spec)\.[\w]+$|(^|/)test_[^/]*\.py$|_test\.(py|go|rs)$|(^|/)conftest\.py$")
1126
+ _TI_SKIP_JS = re.compile(
1127
+ # options object only as the 2nd argument: title (string or identifier), then `, {`
1128
+ r"\b(test|it|describe|suite|context|specify)\s*\(\s*(?:'[^'\n]*'|\"[^\"\n]*\"|`[^`\n]*`|[\w.$]+)\s*,\s*\{[^}]*\b(skip|todo)\s*:\s*(?!(?:false|0|null|undefined)\b)\S"
1129
+ r"|\b(it|test|describe|suite|context|specify)\s*\.\s*(skip|only|todo)\b"
1130
+ r"|\bx(it|test|describe)\s*\(|\bf(it|describe)\s*\("
1131
+ r"|\b(t|ctx)\s*\.\s*(skip|todo)\s*\(")
1132
+ _TI_SKIP_PY = re.compile(
1133
+ r"\b(?:pytest\.)?mark\.(skip|skipif|xfail)\b|\bpytest\.(skip|xfail|importorskip)\s*\("
1134
+ r"|@(?:unittest\.)?(skip|skipIf|skipUnless|expectedFailure)\b|\bSkipTest\b|\bskipTest\s*\(")
1135
+ _TI_SKIP_GO = re.compile(r"\b[tb]\.Skip(f|Now)?\s*\(")
1136
+ _TI_SKIP_RS = re.compile(r"#\[ignore")
1137
+ _TI_SKIP_BY_EXT = (
1138
+ (("js", "jsx", "ts", "tsx", "mjs", "cjs"), _TI_SKIP_JS),
1139
+ (("py",), _TI_SKIP_PY), (("go",), _TI_SKIP_GO), (("rs",), _TI_SKIP_RS))
1140
+ _TI_ASSERT = re.compile(r"\bassert|\bexpect\(|\.should\b|\.to[A-Z]\w*\(|\bt\.(Error|Fatal)")
1141
+ _TI_PKG_KEYS = ("jest", "mocha", "vitest", "ava")
1142
+
1143
+
1144
+ def _ti_skip_re(path):
1145
+ ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
1146
+ for exts, rx in _TI_SKIP_BY_EXT:
1147
+ if ext in exts:
1148
+ return rx
1149
+ return None
1150
+
1151
+
1152
+ def _ti_run(repo, *args):
1153
+ """Run a read-only git command; returns text decoded from bytes (so a lone
1154
+ CR is never a line break; callers split on \\n only), or None on failure."""
1155
+ try:
1156
+ r = subprocess.run(["git", "-c", "core.quotePath=false", *args], cwd=repo,
1157
+ capture_output=True, timeout=30)
1158
+ except Exception:
1159
+ return None
1160
+ if r.returncode != 0:
1161
+ return None
1162
+ return r.stdout.decode("utf-8", "replace")
1163
+
1164
+
1165
+ def _ti_pkg_view(text, _path=""):
1166
+ """Test-relevant slice of a package.json, or None when unparseable."""
1167
+ try:
1168
+ d = json.loads(text) if text.strip() else {}
1169
+ except ValueError:
1170
+ return None
1171
+ if not isinstance(d, dict):
1172
+ return None
1173
+ scripts = d.get("scripts") if isinstance(d.get("scripts"), dict) else {}
1174
+ view = {k: v for k, v in scripts.items() if re.match(r"^(pre|post)?test(:.*)?$", k)}
1175
+ return {"scripts": view, **{k: d[k] for k in _TI_PKG_KEYS if k in d}}
1176
+
1177
+
1178
+ _TI_PYTEST_KEYS = ("addopts", "testpaths", "python_files", "python_functions",
1179
+ "python_classes", "norecursedirs")
1180
+
1181
+
1182
+ def _ti_pytest_view(text, path):
1183
+ """The test-selection keys of a pytest config, or None when unparseable.
1184
+ Parsed (tomllib / configparser), so a multi-line addopts edit is seen.
1185
+ ponytail: without tomllib (python < 3.11) try tomli, else compare only the
1186
+ whitespace-normalized [tool.pytest.ini_options] section text."""
1187
+ if not text.strip():
1188
+ return {}
1189
+ if path.endswith(".toml"):
1190
+ try:
1191
+ try:
1192
+ import tomllib
1193
+ except ImportError:
1194
+ import tomli as tomllib
1195
+ except ImportError:
1196
+ m = re.search(r"^\[tool\.pytest\.ini_options\][ \t]*(?:#.*)?$(.*?)(?=^\[[^\[\]\n]+\][ \t]*(?:#.*)?$|\Z)",
1197
+ text, re.M | re.S)
1198
+ return {"ini_options": " ".join(m.group(1).split())} if m else {}
1199
+ try:
1200
+ d = tomllib.loads(text)
1201
+ except Exception:
1202
+ return None
1203
+ t = d.get("tool", {}).get("pytest", {})
1204
+ t = t.get("ini_options", t) if isinstance(t, dict) else {}
1205
+ return {k: t[k] for k in _TI_PYTEST_KEYS if k in t}
1206
+ import configparser
1207
+ cp = configparser.ConfigParser(interpolation=None, strict=False)
1208
+ try:
1209
+ cp.read_string(text)
1210
+ except Exception:
1211
+ return None
1212
+ out = {}
1213
+ for sec in ("pytest", "tool:pytest"):
1214
+ if cp.has_section(sec):
1215
+ for k in _TI_PYTEST_KEYS:
1216
+ if cp.has_option(sec, k):
1217
+ out[sec + "." + k] = cp.get(sec, k)
1218
+ return out
1219
+
1220
+
1221
+ _TI_SELECT_PY = re.compile(r"\bcollect_ignore(?:_glob)?\b|\bpytest_collection_modifyitems\b")
1222
+ _TI_PYTEST_NAMED = re.compile(r"(^|/)(conftest\.py|\.?pytest\.ini|tox\.ini)$")
1223
+
1224
+
1225
+ def _ti_base_has_pytest_config(target_dir, base):
1226
+ names = _ti_run(target_dir, "ls-tree", "-r", "--name-only", "-z", base) or ""
1227
+ for n in names.split("\0"):
1228
+ if _TI_PYTEST_NAMED.search(n):
1229
+ return True
1230
+ if n.endswith(("pyproject.toml", "setup.cfg")) and \
1231
+ _ti_pytest_view(_ti_run(target_dir, "show", "%s:./%s" % (base, n)) or "", n):
1232
+ return True
1233
+ return False
1234
+
1235
+
1236
+ def _ti_added_config_selects(path, text):
1237
+ """True when a newly added pytest config changes test selection (unparseable counts)."""
1238
+ if path.endswith(".py"):
1239
+ return bool(_TI_SELECT_PY.search(text))
1240
+ if path.endswith((".ini", ".toml", ".cfg")):
1241
+ v = _ti_pytest_view(text, path)
1242
+ return v is None or bool(v)
1243
+ return False
1244
+
1245
+
1246
+ def _collect_tests_integrity(target_dir, base):
1247
+ """Return {"weakened": [...], "assertions_edited": [...], "disclosed": [...]} or None.
1248
+
1249
+ Derived from base vs the final working tree (committed, staged, unstaged and
1250
+ untracked). None when there is no usable diff (no repo, no base)."""
1251
+ if not base:
1252
+ return None
1253
+ names = _ti_run(target_dir, "diff", "--relative", "--name-status", "-M", "-z", base, "--")
1254
+ if names is None:
1255
+ return None
1256
+ weakened, edited, disclosed = [], [], []
1257
+ greenfield = base == _empty_tree_sha(target_dir)
1258
+ toks = names.split("\0")
1259
+ entries, i = [], 0 # (status, old_path, new_path)
1260
+ while i < len(toks) and toks[i]:
1261
+ st = toks[i][:1]
1262
+ if st in "RC" and i + 2 < len(toks):
1263
+ entries.append((st, toks[i + 1], toks[i + 2]))
1264
+ i += 3
1265
+ else:
1266
+ entries.append((st, toks[i + 1] if i + 1 < len(toks) else "", toks[i + 1] if i + 1 < len(toks) else ""))
1267
+ i += 2
1268
+ untracked = _ti_run(target_dir, "ls-files", "--others", "--exclude-standard", "-z") or ""
1269
+ for u in untracked.split("\0"):
1270
+ if u and not u.startswith(".loki/"):
1271
+ entries.append(("A", u, u))
1272
+ for st, old, new in entries:
1273
+ if st in ("D", "R") and _TI_TEST_FILE.search(old):
1274
+ weakened.append("test file %s: %s" % ("deleted" if st == "D" else "renamed", old))
1275
+ if not greenfield:
1276
+ for st, old, new in entries:
1277
+ path = new if st != "D" else old
1278
+ is_always = bool(_TI_CFG_ALWAYS.search(path))
1279
+ if not (is_always or _TI_CFG_SHARED.search(path)):
1280
+ continue
1281
+ if st == "A": # no base version: disclose, unless it re-selects tests over a base pytest config
1282
+ try:
1283
+ with open(os.path.join(target_dir, new), encoding="utf-8", errors="replace") as h:
1284
+ text = h.read()
1285
+ except OSError:
1286
+ text = ""
1287
+ if _ti_added_config_selects(path, text) and _ti_base_has_pytest_config(target_dir, base):
1288
+ weakened.append("test runner config changed: " + path)
1289
+ else:
1290
+ disclosed.append("new test runner config: " + path)
1291
+ elif is_always:
1292
+ weakened.append("test runner config changed: " + path)
1293
+ else:
1294
+ before = _ti_run(target_dir, "show", "%s:./%s" % (base, old)) or ""
1295
+ try:
1296
+ with open(os.path.join(target_dir, new), encoding="utf-8", errors="replace") as h:
1297
+ after = h.read()
1298
+ except OSError:
1299
+ after = ""
1300
+ view = _ti_pkg_view if path.endswith("package.json") else _ti_pytest_view
1301
+ b, a = view(before, path), view(after, path)
1302
+ if b is None or a is None or b != a:
1303
+ weakened.append("test runner config changed: " + path)
1304
+ # Skip markers: added lines of tracked test files, and the content of untracked ones.
1305
+ patch = _ti_run(target_dir, "diff", "--relative", "-U0", "-M", base, "--") or ""
1306
+ cur, rx, removed_assert = None, None, False
1307
+ for line in patch.split("\n") + ["diff --git a/ b/"]:
1308
+ if line.startswith("diff --git "):
1309
+ if cur and removed_assert and cur not in edited:
1310
+ edited.append(cur)
1311
+ cur, rx, removed_assert = None, None, False
1312
+ elif line.startswith("+++ b/"):
1313
+ cur = line[6:] if _TI_TEST_FILE.search(line[6:]) else None
1314
+ rx = _ti_skip_re(cur) if cur else None
1315
+ elif cur and line.startswith("+") and rx and rx.search(line):
1316
+ weakened.append("skip added in %s" % cur)
1317
+ elif cur and line.startswith("-") and not line.startswith("---") \
1318
+ and _TI_ASSERT.search(line):
1319
+ removed_assert = True
1320
+ for u in untracked.split("\0"):
1321
+ if not u or u.startswith(".loki/") or not _TI_TEST_FILE.search(u):
1322
+ continue
1323
+ rx = _ti_skip_re(u)
1324
+ if not rx:
1325
+ continue
1326
+ try:
1327
+ with open(os.path.join(target_dir, u), "rb") as h:
1328
+ text = h.read().decode("utf-8", "replace")
1329
+ except OSError:
1330
+ continue
1331
+ if any(rx.search(ln) for ln in text.split("\n")):
1332
+ weakened.append("skip added in %s" % u)
1333
+ uniq = []
1334
+ for w in weakened:
1335
+ if w not in uniq:
1336
+ uniq.append(w)
1337
+ if not uniq and not edited and not disclosed:
1338
+ return None
1339
+ return {"weakened": uniq, "assertions_edited": edited, "disclosed": disclosed}
1340
+
1341
+
1118
1342
  def _collect_iterations(loki_dir):
1119
1343
  completed = _read_json(os.path.join(loki_dir, "queue", "completed.json"), default=[])
1120
1344
  failed = _read_json(os.path.join(loki_dir, "queue", "failed.json"), default=[])
@@ -1455,6 +1679,10 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1455
1679
  # only, never read by _compute_headline / _compute_degraded.
1456
1680
  if journey:
1457
1681
  facts["journey"] = journey
1682
+ # A-118: additive, present only when the diff touched test integrity.
1683
+ _ti = _collect_tests_integrity(target_dir, diff_base_sha)
1684
+ if _ti:
1685
+ facts["tests_integrity"] = _ti
1458
1686
  ablation = _collect_ablation(loki_dir)
1459
1687
  if ablation is not None:
1460
1688
  facts["ablation"] = ablation
@@ -1517,6 +1745,24 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1517
1745
  "post_headline": True,
1518
1746
  })
1519
1747
 
1748
+ # D47: edited assertion lines are disclosed but never move the headline or rc.
1749
+ _ae = (facts.get("tests_integrity") or {}).get("assertions_edited")
1750
+ if _ae:
1751
+ degraded.append({
1752
+ "item": "tests_integrity:assertions_edited",
1753
+ "status": "inconclusive",
1754
+ "reason": "assertion lines edited in: " + ", ".join(_ae),
1755
+ "post_headline": True,
1756
+ })
1757
+ _di = (facts.get("tests_integrity") or {}).get("disclosed")
1758
+ if _di:
1759
+ degraded.append({
1760
+ "item": "tests_integrity:config_added",
1761
+ "status": "inconclusive",
1762
+ "reason": "; ".join(_di),
1763
+ "post_headline": True,
1764
+ })
1765
+
1520
1766
  honesty = {
1521
1767
  "headline": headline,
1522
1768
  "degraded": degraded,
@@ -1653,6 +1899,10 @@ def _compute_degraded(facts):
1653
1899
  out.append({"item": "security", "status": "findings",
1654
1900
  "reason": "%s un-waived HIGH security finding(s)"
1655
1901
  % sec.get("high_active")})
1902
+ ti = facts.get("tests_integrity") or {}
1903
+ if ti.get("weakened"):
1904
+ out.append({"item": "tests_integrity", "status": "failed",
1905
+ "reason": "tests weakened: " + "; ".join(ti["weakened"])})
1656
1906
  git = facts.get("git") or {}
1657
1907
  if not (git.get("diff") or {}).get("count"):
1658
1908
  out.append({"item": "git.diff", "status": "not_run",
@@ -1748,6 +1998,7 @@ def _compute_headline(facts, degraded):
1748
1998
  if _is_exogenous(g))
1749
1999
  or sec_high
1750
2000
  or fn_failed
2001
+ or bool((facts.get("tests_integrity") or {}).get("weakened"))
1751
2002
  )
1752
2003
  if any_failed:
1753
2004
  return "NOT VERIFIED"
@@ -331,6 +331,7 @@ def _compute_headline(facts, degraded):
331
331
  if _is_exogenous(g))
332
332
  or sec_high
333
333
  or fn_failed
334
+ or bool((facts.get("tests_integrity") or {}).get("weakened"))
334
335
  )
335
336
  if any_failed:
336
337
  return "NOT VERIFIED"
package/autonomy/loki CHANGED
@@ -1850,6 +1850,48 @@ show_help_aliases() {
1850
1850
  echo "See internal/CLI-CONSOLIDATION-DESIGN.md for the full migration plan."
1851
1851
  }
1852
1852
 
1853
+ # D51: bare `loki`. Reuse a running dashboard, else start one on a free
1854
+ # loopback port, then open /start (or just print the URL when headless).
1855
+ loki_ui_free_port() {
1856
+ python3 - "$1" <<'PY'
1857
+ import socket, sys
1858
+ for p in range(int(sys.argv[1]), int(sys.argv[1]) + 100):
1859
+ s = socket.socket()
1860
+ try:
1861
+ s.bind(("127.0.0.1", p))
1862
+ except OSError:
1863
+ continue
1864
+ finally:
1865
+ s.close()
1866
+ print(p)
1867
+ break
1868
+ PY
1869
+ }
1870
+
1871
+ cmd_ui() {
1872
+ local port="" pid=""
1873
+ if [ -f "$DASHBOARD_PID_FILE" ]; then
1874
+ pid=$(cat "$DASHBOARD_PID_FILE" 2>/dev/null)
1875
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
1876
+ port=$(cat "${DASHBOARD_PID_DIR}/port" 2>/dev/null)
1877
+ fi
1878
+ fi
1879
+ if [ -z "$port" ]; then
1880
+ port=$(loki_ui_free_port "${LOKI_DASHBOARD_PORT:-$DASHBOARD_DEFAULT_PORT}")
1881
+ [ -n "$port" ] || { echo "loki: no free local port found" >&2; return 1; }
1882
+ # The WS-origin allowlist must name the chosen port on both loopback names.
1883
+ LOKI_PROJECT_DIR="$PWD" \
1884
+ LOKI_DASHBOARD_CORS="http://localhost:$port,http://127.0.0.1:$port" \
1885
+ cmd_dashboard_start --host 127.0.0.1 --port "$port" >&2 || return 1
1886
+ fi
1887
+ local url="http://127.0.0.1:${port}/start"
1888
+ if [ "${LOKI_HEADLESS:-0}" = "1" ] || [ "${1:-}" = "--no-open" ]; then
1889
+ echo "$url"
1890
+ else
1891
+ loki_open_url "$url" || echo "$url"
1892
+ fi
1893
+ }
1894
+
1853
1895
  # Tight newcomer landing for the bare `loki` invocation (no args).
1854
1896
  # v7.28.x UX: the full 70-command table lives in `loki help`; a no-args run
1855
1897
  # now shows only a calm ~12-line orientation so the "what do I do first"
@@ -3397,7 +3439,7 @@ cmd_start() {
3397
3439
  ${LOKI_MIROFISH_ROUNDS:+--max-rounds "$LOKI_MIROFISH_ROUNDS"} \
3398
3440
  ${LOKI_MIROFISH_TIMEOUT:+--timeout "$LOKI_MIROFISH_TIMEOUT"}
3399
3441
  # PID is tracked in .loki/mirofish/pipeline-state.json by the adapter
3400
- echo -e "${CYAN}MiroFish pipeline started in background. Check status: loki mirofish status${NC}"
3442
+ echo -e "${CYAN}MiroFish pipeline started in background. Status: $LOKI_DIR/mirofish/pipeline-state.json${NC}"
3401
3443
  else
3402
3444
  echo -e "${YELLOW}Warning: PRD validation for MiroFish failed. Continuing without.${NC}"
3403
3445
  unset MIROFISH_URL
@@ -21586,11 +21628,15 @@ main() {
21586
21628
  exit 1
21587
21629
  fi
21588
21630
 
21589
- if [ $# -eq 0 ]; then
21590
- # v7.28.x UX: bare `loki` shows a tight newcomer landing, not the full
21591
- # command wall. `loki help`/`--help` still print show_help unchanged.
21592
- show_landing
21593
- exit 0
21631
+ if [ $# -eq 0 ] || { [ $# -eq 1 ] && [ "$1" = "--no-open" ]; }; then
21632
+ # D51: bare `loki` starts the local UI. LOKI_LANDING=1 keeps the v7.28
21633
+ # newcomer landing (tests and scripts that parse it set it).
21634
+ if [ "${LOKI_LANDING:-0}" = "1" ]; then
21635
+ show_landing
21636
+ exit 0
21637
+ fi
21638
+ cmd_ui "$@"
21639
+ exit $?
21594
21640
  fi
21595
21641
 
21596
21642
  local command="$1"
package/autonomy/run.sh CHANGED
@@ -256,7 +256,10 @@ if [ -z "${LOKI_QUICK_INNER:-}" ] && [ "${LOKI_VERBOSE:-0}" != "1" ] && [ "${BAS
256
256
  if command -v python3 >/dev/null 2>&1 && python3 -c 'import os,signal' >/dev/null 2>&1; then
257
257
  _qexec=(python3 -c 'import os,signal,sys;signal.signal(signal.SIGPIPE,signal.SIG_DFL);os.execv(sys.argv[1],sys.argv[1:])' "${_qexec[@]}")
258
258
  fi
259
- ( trap - INT QUIT; LOKI_QUICK_DIR="$_qd" LOKI_QUICK_OUTER_PID=$$ LOKI_QUICK_INNER=1 exec "${_qexec[@]}" >"$_qd/quick-run.log" ) &
259
+ # A background job gets stdin=/dev/null. Only a TTY needs it back (pause-mode
260
+ # keypress resume); a pipe would block the provider, which reads inherited stdin.
261
+ if [ -t 0 ]; then exec 3<&0; else exec 3</dev/null; fi
262
+ ( trap - INT QUIT; LOKI_QUICK_DIR="$_qd" LOKI_QUICK_OUTER_PID=$$ LOKI_QUICK_INNER=1 exec "${_qexec[@]}" >"$_qd/quick-run.log" <&3 3<&- ) &
260
263
  _qpid=$!
261
264
  trap 'kill -INT "$_qpid" 2>/dev/null' INT
262
265
  trap 'kill -TERM "$_qpid" 2>/dev/null' TERM HUP
@@ -265,8 +268,10 @@ if [ -z "${LOKI_QUICK_INNER:-}" ] && [ "${LOKI_VERBOSE:-0}" != "1" ] && [ "${BAS
265
268
  trap - INT TERM HUP
266
269
  _qw=$(grep -cF '[WARN]' "$_qd/quick-run.log" 2>/dev/null); _qw="${_qw:-0}"
267
270
  if [ "$_qrc" -ne 0 ] || [ ! -s "$_qd/quick-receipt.txt" ]; then
268
- _qr=$(grep -E '\[(ERROR|WARN)\]|[Ss]topped|[Ff]ailed' "$_qd/quick-run.log" 2>/dev/null | tail -n 3)
269
- echo "${_qr:-$(tail -n 3 "$_qd/quick-run.log" 2>/dev/null)}"
271
+ # Anchored to log_error/log_warn lines (optional ANSI prefix) and capped at 200
272
+ # chars so a long prompt line mentioning "failed" is never dumped.
273
+ _qr=$(grep -E $'^(\033\\[[0-9;]*m)?\\[(ERROR|WARN)\\]' "$_qd/quick-run.log" 2>/dev/null | tail -n 3 | cut -c1-200)
274
+ echo "${_qr:-$(tail -n 3 "$_qd/quick-run.log" 2>/dev/null | cut -c1-200)}"
270
275
  fi
271
276
  [ "$_qw" -gt 0 ] && echo "Warnings: $_qw (see $_qd/quick-run.log)"
272
277
  cat "$_qd/quick-receipt.txt" 2>/dev/null
@@ -11252,6 +11257,27 @@ _loki_proof_json_for_pr() {
11252
11257
  }
11253
11258
 
11254
11259
  # A-134: compact quiet-mode receipt for the outer shell (see top of file) to print.
11260
+ # A-118 / D47: a quick run that weakened its own tests exits 3. Prints the rc to use:
11261
+ # only ever RAISES a 0 (never lowers a non-zero rc); NOT VERIFIED from unproven gates alone keeps 0.
11262
+ _loki_quick_integrity_rc() {
11263
+ local rc="${1:-0}" ld="${TARGET_DIR:-.}/.loki" rid pj
11264
+ [ "$rc" = "0" ] || { echo "$rc"; return 0; }
11265
+ rid="$(cat "$ld/state/last-proof-id.txt" 2>/dev/null || true)"
11266
+ case "$rid" in '' | *[!A-Za-z0-9._-]*) echo 0; return 0 ;; esac
11267
+ pj="$ld/proofs/$rid/proof.json"
11268
+ if [ -f "$pj" ] && python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
11269
+ import json
11270
+ d = json.load(open(sys.argv[1]))
11271
+ g = (d.get('facts') or {}).get('git') or {}
11272
+ if sys.argv[2] and g.get('head_sha') != sys.argv[2]:
11273
+ sys.exit(1)
11274
+ sys.exit(0 if any(isinstance(x, dict) and x.get('item') == 'tests_integrity' and x.get('status') == 'failed' for x in ((d.get('honesty') or {}).get('degraded') or [])) else 1)" "$pj" "$(git -C "${TARGET_DIR:-.}" rev-parse HEAD 2>/dev/null || true)" 2>/dev/null; then
11275
+ echo 3
11276
+ else
11277
+ echo 0
11278
+ fi
11279
+ }
11280
+
11255
11281
  _loki_quick_receipt_write() {
11256
11282
  local ld="${TARGET_DIR:-.}/.loki" rid pj
11257
11283
  rid="$(cat "$ld/state/last-proof-id.txt" 2>/dev/null || true)"
@@ -11266,8 +11292,10 @@ v = d.get('verification') or {}
11266
11292
  if sys.argv[3] and g.get('head_sha') != sys.argv[3]:
11267
11293
  print('Evidence Receipt: unavailable (final proof generation failed; newest proof predates the session commit)')
11268
11294
  sys.exit(0)
11269
- deg = [str(x.get('item') or '').split(':')[-1] for x in ((d.get('honesty') or {}).get('degraded') or []) if isinstance(x, dict)]
11270
- notes = ([] if (v.get('gpg_signature') or v.get('attestation')) else ['unsigned']) + (['%d not proven: %s' % (len(deg), ', '.join(deg))] if deg else [])
11295
+ dg = [x for x in ((d.get('honesty') or {}).get('degraded') or []) if isinstance(x, dict)]
11296
+ tw = [str(x.get('reason') or 'tests weakened') for x in dg if x.get('item') == 'tests_integrity' and x.get('status') == 'failed']
11297
+ deg = [str(x.get('item') or '').split(':')[-1] for x in dg if x.get('item') != 'tests_integrity']
11298
+ notes = tw[:1] + ([] if (v.get('gpg_signature') or v.get('attestation')) else ['unsigned']) + (['%d not proven: %s' % (len(deg), ', '.join(deg))] if deg else [])
11271
11299
  print('Evidence Receipt: ' + str((d.get('honesty') or {}).get('headline') or 'unavailable') + (' (' + '; '.join(notes) + ')' if notes else ''))
11272
11300
  print('receipt_sha256: ' + str(v.get('hash') or ''))
11273
11301
  print('Head sha: ' + str(g.get('head_sha') or '') + ' Diff sha256: ' + str(g.get('diff_sha256') or ''))
@@ -29402,6 +29430,8 @@ except Exception:
29402
29430
  if [ "${LOKI_PROOF:-1}" != "0" ]; then
29403
29431
  generate_proof_of_run "$result" || true
29404
29432
  fi
29433
+ # A-118: a quick run that weakened its tests exits 3 (quiet and verbose alike).
29434
+ ! _loki_is_quick_prd "${PRD_PATH:-}" || result="$(_loki_quick_integrity_rc "$result")"
29405
29435
  [ -z "${LOKI_QUICK_INNER:-}" ] || _loki_quick_receipt_write
29406
29436
  # Trusted post-session step. NOT wrapped in _loki_with_github_tokens as a
29407
29437
  # whole (round 5): its push (_loki_trusted_push) and gh calls each re-grant
@@ -3071,7 +3071,7 @@ sys.exit(0 if ok else 3)" "$lib" "$pj" 2>/dev/null || rc=$?
3071
3071
  # Provenance: the deploy gate's own verdict (Ed25519 attestation against the local
3072
3072
  # key plus LOKI_RECEIPT_RETIRED_PUBKEYS; unknown kid or no key is UNCHECKED).
3073
3073
  # Defined in autonomy/loki, so it is absent when verify.sh runs standalone.
3074
- declare -f _deploy_receipt_verdict >/dev/null 2>&1 || return 0
3074
+ declare -f _deploy_receipt_verdict >/dev/null 2>&1 || { echo "attestation: UNCHECKED (run via loki verify)"; return 0; }
3075
3075
  local att
3076
3076
  att="$(_deploy_receipt_verdict "$pj")"
3077
3077
  # D47: UNSIGNED is never a pass; --allow-unsigned / LOKI_VERIFY_ALLOW_UNSIGNED=1 accepts it, said aloud.
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "10.5.17"
10
+ __version__ = "10.5.20"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: