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.
- package/README.md +877 -30
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/lib/proof-generator.py +251 -0
- package/autonomy/lib/proof-verify.py +1 -0
- package/autonomy/loki +52 -6
- package/autonomy/run.sh +35 -5
- package/autonomy/verify.sh +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/api_start.py +463 -0
- package/dashboard/server.py +31 -0
- package/dashboard/static/start.html +100 -0
- package/docs/COMPARISON.md +6 -19
- package/docs/COMPETITIVE-ANALYSIS.md +6 -41
- package/docs/CRASH-REPORTING-PLAN.md +1 -1
- package/docs/ENTERPRISE-IDENTITY-ROADMAP.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/PRIVACY.md +1 -1
- package/docs/R8-SHAREABLE-TEAM-ASSETS-PLAN.md +1 -1
- package/docs/WANG-PRINCIPLES-PLAN.md +1 -1
- package/docs/architecture/STATE-MACHINES.md +18 -27
- package/docs/architecture/bmad-integration-validation.md +2 -2
- package/docs/audit-logging.md +12 -86
- package/docs/authentication.md +4 -4
- package/docs/authorization.md +15 -47
- package/docs/auto-claude-comparison.md +1 -33
- package/docs/certification/01-core-concepts/lab.md +3 -3
- package/docs/certification/01-core-concepts/lesson.md +11 -22
- package/docs/certification/01-core-concepts/quiz.md +7 -7
- package/docs/certification/02-enterprise-features/lab.md +0 -2
- package/docs/certification/02-enterprise-features/lesson.md +0 -10
- package/docs/certification/04-production-deployment/lab.md +1 -1
- package/docs/certification/05-troubleshooting/lab.md +2 -2
- package/docs/certification/README.md +1 -1
- package/docs/certification/answer-key.md +37 -20
- package/docs/certification/certification-exam.md +7 -7
- package/docs/competitive/emergence-others-analysis.md +3 -3
- package/docs/competitive/replit-lovable-analysis.md +2 -2
- package/docs/cursor-comparison.md +2 -2
- package/docs/dashboard-guide.md +14 -2
- package/docs/dev/architecture-reference.md +2 -2
- package/docs/dev/project-structure.md +1 -1
- package/docs/enterprise/architecture.md +11 -8
- package/docs/enterprise/integration-cookbook.md +23 -14
- package/docs/enterprise/migration.md +5 -9
- package/docs/enterprise/performance.md +3 -6
- package/docs/enterprise/sdk-guide.md +1 -1
- package/docs/enterprise/security.md +8 -17
- package/docs/exit-codes.md +14 -0
- package/docs/git-workflow.md +4 -62
- package/docs/network-security.md +3 -13
- package/docs/plans/ULTRAPLAN-FORGE-BAAS.md +1 -1
- package/docs/prd-purple-lab-platform-v2.md +1 -1
- package/docs/prd-purple-lab-platform.md +1 -3
- package/docs/research-2026-07/GAP-ANALYSIS-BACKLOG.md +1 -1
- package/docs/show-hn-post.md +3 -4
- package/docs/siem-integration.md +8 -10
- package/docs/test-scenarios/edge-cases.md +2 -2
- package/docs/test-scenarios/enterprise-scenarios.md +4 -4
- package/docs/v10/BOARD.md +7 -5
- package/docs/v10/GUIDE.md +5 -3
- package/docs/v10/METRICS.md +36 -0
- package/docs/v10/PROGRESS.md +42 -0
- package/docs/walkthrough/README.md +1 -7
- package/loki-ts/dist/loki.js +370 -370
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-DxGZSpFg.js → AdminPage-HjtApbcL.js} +1 -1
- package/web-app/dist/assets/{Avatar-BZEp23D8.js → Avatar-BcwLENsU.js} +1 -1
- package/web-app/dist/assets/{Badge-DnDwqDLu.js → Badge-DjJhHJj0.js} +1 -1
- package/web-app/dist/assets/{Button-C9KDP4c1.js → Button-CSMstrCz.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-DRlh3LV2.js → CockpitPage-BHihN-aq.js} +1 -1
- package/web-app/dist/assets/{ComparePage-C_gE45dQ.js → ComparePage-_6JtexDp.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-CL-J9-Cs.js → ErrorBoundary-Cx2fnw3q.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-DABd176Y.js → EvidenceReceiptPanel-DcUQAnju.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-C4lYbm6O.js → GitHubIssuesPanel-1dYpQ5Rx.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-CgiUixwo.js → GitHubPRsPanel-BPdDD1kX.js} +1 -1
- package/web-app/dist/assets/{HomePage-DC99RIrz.js → HomePage-JfmwLGnQ.js} +2 -2
- package/web-app/dist/assets/{LoginPage-Bd72QWNk.js → LoginPage-7SnMHzPc.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CBouFs2K.js → MagicPage-C0IkoOzD.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-c9MyDcBp.js → MetricsPage-BJOZ0L3F.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-ZPhiebiW.js → NotFoundPage-S49UjLyW.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DKwrALUb.js → ProjectPage-Dj_nv2kT.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-X1uZKZC_.js → ProjectsPage-C7thLjOm.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-CiZKZRto.js → SettingsPage-DEcJyfsU.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-k_imKfeh.js → ShowcasePage-B5PshXNg.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-Wx9ESLll.js → SystemSettingsPage-Cnp4BkN7.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-B7upn-I7.js → TeamsPage-1ImpgMzs.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-oJf_VKgh.js → TemplatesPage-BOYKrPX8.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BdfPlgLI.js → TerminalOutput-LPxzwfwm.js} +1 -1
- package/web-app/dist/assets/{activity-BcZTsgWH.js → activity-Hl7iDpX_.js} +1 -1
- package/web-app/dist/assets/{bell-JNEkjWQ2.js → bell-DrBfjAq4.js} +1 -1
- package/web-app/dist/assets/{bot-DMAJobP_.js → bot-6SoklKaZ.js} +1 -1
- package/web-app/dist/assets/{check-DPTry-z2.js → check-DhHuZH8k.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DSqOEtTV.js → chevron-left-BUERGaLO.js} +1 -1
- package/web-app/dist/assets/{circle-alert-BbL8Vysv.js → circle-alert-D7UOw5bc.js} +1 -1
- package/web-app/dist/assets/{clock-BHEr5lGW.js → clock-t3v_i7fP.js} +1 -1
- package/web-app/dist/assets/{cloud-C48PunYo.js → cloud-CP3eLZts.js} +1 -1
- package/web-app/dist/assets/{code-xml-Boa7kRqT.js → code-xml-DU-CmwVx.js} +1 -1
- package/web-app/dist/assets/{database-Eu35Gv7z.js → database-J27fE_Ot.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-BO9fCnHW.js → dollar-sign-BdU_HR9C.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-DHsSDIL_.js → file-code-corner-Cu4A1aIH.js} +1 -1
- package/web-app/dist/assets/{file-plus-BYiQLe5l.js → file-plus-BTli76r5.js} +1 -1
- package/web-app/dist/assets/{globe-BPZ5sQI2.js → globe-Bz3m1c_-.js} +1 -1
- package/web-app/dist/assets/{hammer-DlOLTsH3.js → hammer-HNN6jr92.js} +1 -1
- package/web-app/dist/assets/{index-BrgkuqSA.js → index-CgK4xqUy.js} +3 -3
- package/web-app/dist/assets/{layers-DMNdn1GR.js → layers-DhuXUuM6.js} +1 -1
- package/web-app/dist/assets/{loader-circle-_e9fypHL.js → loader-circle-CeBjdh2y.js} +1 -1
- package/web-app/dist/assets/{lock-D_elxMZb.js → lock-DjSqmIVZ.js} +1 -1
- package/web-app/dist/assets/{package-Bw-7g3JI.js → package-NL0AdpIi.js} +1 -1
- package/web-app/dist/assets/{plus-CZeq0pgJ.js → plus-Lbz3hom-.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-D0WMyffs.js → refresh-cw-uKARwuER.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CcKyNwXG.js → rotate-ccw-ByA6XJq1.js} +1 -1
- package/web-app/dist/assets/{scroll-text-C0L9Vqpm.js → scroll-text-BV8Blxig.js} +1 -1
- package/web-app/dist/assets/{server-SgwFv6cd.js → server-Cy3R5hde.js} +1 -1
- package/web-app/dist/assets/{shield-alert-D1tESlfI.js → shield-alert-rE_xXx7r.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-bUtUA15O.js → thumbs-up-CNMITZHD.js} +1 -1
- package/web-app/dist/assets/{trash-2-C9Nv1kKk.js → trash-2-BP3Rr5SU.js} +1 -1
- package/web-app/dist/assets/{trending-down-Bh107AJe.js → trending-down-BE_8lCjD.js} +1 -1
- package/web-app/dist/assets/{trending-up-25BqhLVq.js → trending-up-CF6E9-9a.js} +1 -1
- package/web-app/dist/assets/{upload-DDYtMbN5.js → upload-D-Ui4N6K.js} +1 -1
- package/web-app/dist/assets/{usePolling-BSDT0il_.js → usePolling-DIVX97OV.js} +1 -1
- package/web-app/dist/assets/{user-CKpUEUhK.js → user-7rTiTfDj.js} +1 -1
- package/web-app/dist/index.html +1 -1
- 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.
|
|
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
|
|
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.
|
|
473
|
+
**v10.5.20 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
10.5.
|
|
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"
|
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.
|
|
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
|
-
#
|
|
21591
|
-
#
|
|
21592
|
-
|
|
21593
|
-
|
|
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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
11270
|
-
|
|
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
|
package/autonomy/verify.sh
CHANGED
|
@@ -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.
|