nexo-brain 7.17.3 → 7.17.5
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/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/agent_runner.py +77 -16
- package/src/cli.py +33 -2
- package/src/doctor/providers/runtime.py +158 -0
- package/src/scripts/check-context.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
3
|
+
"version": "7.17.5",
|
|
4
4
|
"description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "NEXO Brain",
|
package/README.md
CHANGED
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
[Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
|
|
20
20
|
|
|
21
|
-
Version `7.17.
|
|
21
|
+
Version `7.17.5` is the current packaged-runtime line. Patch release over v7.17.4 - `nexo --version --json` now returns fast machine-readable update status (`installed`, `latest`, `hasUpdate`, `unknown`, `latestSource`) while the human output keeps the legacy `nexo vX` line plus a compact latest/installed status line for Desktop compatibility.
|
|
22
|
+
|
|
23
|
+
Previously in `7.17.4`: corrective patch over v7.17.3 - automation runners now keep full NEXO discipline for real background agents while strict JSON children stay clean, and runtime doctor/metrics expose caller coverage and Guardian injection telemetry instead of hiding blind spots.
|
|
24
|
+
|
|
25
|
+
Previously in `7.17.3`: corrective patch over v7.17.2 - standalone Brain install/update no longer aborts when the Desktop-only `qwen3-0.6b-q4-local-presence` model is not bundled or already cached locally. Required Brain warmups stay strict; only the optional local-presence GGUF now degrades cleanly.
|
|
22
26
|
|
|
23
27
|
Previously in `7.17.2`: patch release over v7.17.1 - email-monitor now guards its `/tmp/nexo-*` draft buffers before writing, morning-agent closes interrupted/stale briefing claims deterministically, and Codex managed config migrates from the legacy `codex_hooks` flag to `[features].hooks`.
|
|
24
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.17.
|
|
3
|
+
"version": "7.17.5",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
|
|
6
6
|
"homepage": "https://nexo-brain.com",
|
package/src/agent_runner.py
CHANGED
|
@@ -273,6 +273,7 @@ def _record_automation_end(
|
|
|
273
273
|
{
|
|
274
274
|
"warnings": (telemetry or {}).get("warnings") or [],
|
|
275
275
|
"raw": (telemetry or {}).get("raw") or {},
|
|
276
|
+
"automation_contract": (telemetry or {}).get("automation_contract") or "",
|
|
276
277
|
},
|
|
277
278
|
ensure_ascii=False,
|
|
278
279
|
)
|
|
@@ -922,10 +923,11 @@ def _build_codex_prompt(
|
|
|
922
923
|
output_format: str = "",
|
|
923
924
|
append_system_prompt: str = "",
|
|
924
925
|
allowed_tools: str = "",
|
|
926
|
+
include_protocol_contract: bool = True,
|
|
925
927
|
) -> str:
|
|
926
|
-
protocol_contract = render_core_prompt("codex-protocol-contract")
|
|
927
928
|
instructions: list[str] = []
|
|
928
|
-
|
|
929
|
+
if include_protocol_contract:
|
|
930
|
+
instructions.append(render_core_prompt("codex-protocol-contract"))
|
|
929
931
|
if append_system_prompt:
|
|
930
932
|
instructions.append(f"SYSTEM INSTRUCTIONS:\n{append_system_prompt}")
|
|
931
933
|
if output_format and output_format.lower() == "text":
|
|
@@ -1047,6 +1049,35 @@ BARE_MODE_SAFE_CALLERS: frozenset[str] = frozenset({
|
|
|
1047
1049
|
"deep-sleep/synthesize",
|
|
1048
1050
|
})
|
|
1049
1051
|
|
|
1052
|
+
# Execution contracts keep background agents disciplined without polluting
|
|
1053
|
+
# machine-only child calls that must return strict JSON.
|
|
1054
|
+
AUTOMATION_CONTRACT_FULL_NEXO_AGENT = "full_nexo_agent"
|
|
1055
|
+
AUTOMATION_CONTRACT_STRICT_CHILD = "strict_child_json"
|
|
1056
|
+
AUTOMATION_CONTRACT_PUBLIC_CHILD = "public_isolated_child"
|
|
1057
|
+
AUTOMATION_CONTRACT_DEFAULT = AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1058
|
+
|
|
1059
|
+
FULL_NEXO_AGENT_CALLERS: frozenset[str] = frozenset({
|
|
1060
|
+
"catchup/morning",
|
|
1061
|
+
"daily_self_audit",
|
|
1062
|
+
"email_monitor",
|
|
1063
|
+
"evolution/run",
|
|
1064
|
+
"followup_runner",
|
|
1065
|
+
"immune/scan",
|
|
1066
|
+
"postmortem_consolidator",
|
|
1067
|
+
"sleep/nightly",
|
|
1068
|
+
})
|
|
1069
|
+
|
|
1070
|
+
STRICT_CHILD_CALLERS: frozenset[str] = frozenset({
|
|
1071
|
+
"automation_probe",
|
|
1072
|
+
"check_context",
|
|
1073
|
+
"deep-sleep/extract",
|
|
1074
|
+
"deep-sleep/synthesize",
|
|
1075
|
+
"learning_validator",
|
|
1076
|
+
"morning_agent",
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
PUBLIC_CHILD_CALLERS: frozenset[str] = frozenset()
|
|
1080
|
+
|
|
1050
1081
|
MACHINE_ONLY_LANGUAGE_CONTRACT_CALLERS: frozenset[str] = frozenset({
|
|
1051
1082
|
"automation_probe",
|
|
1052
1083
|
"check_context",
|
|
@@ -1054,6 +1085,19 @@ MACHINE_ONLY_LANGUAGE_CONTRACT_CALLERS: frozenset[str] = frozenset({
|
|
|
1054
1085
|
})
|
|
1055
1086
|
|
|
1056
1087
|
|
|
1088
|
+
def _automation_contract_for_caller(caller: str) -> str:
|
|
1089
|
+
clean = str(caller or "").strip()
|
|
1090
|
+
if clean in STRICT_CHILD_CALLERS:
|
|
1091
|
+
return AUTOMATION_CONTRACT_STRICT_CHILD
|
|
1092
|
+
if clean in PUBLIC_CHILD_CALLERS:
|
|
1093
|
+
return AUTOMATION_CONTRACT_PUBLIC_CHILD
|
|
1094
|
+
return AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _caller_uses_global_discipline(caller: str) -> bool:
|
|
1098
|
+
return _automation_contract_for_caller(caller) == AUTOMATION_CONTRACT_FULL_NEXO_AGENT
|
|
1099
|
+
|
|
1100
|
+
|
|
1057
1101
|
def _should_apply_operator_language_contract(caller: str) -> bool:
|
|
1058
1102
|
clean = str(caller or "").strip()
|
|
1059
1103
|
if not clean:
|
|
@@ -1140,7 +1184,8 @@ def run_automation_prompt(
|
|
|
1140
1184
|
if mapped_effort and not reasoning_effort:
|
|
1141
1185
|
reasoning_effort = mapped_effort
|
|
1142
1186
|
|
|
1143
|
-
|
|
1187
|
+
automation_contract = _automation_contract_for_caller(caller)
|
|
1188
|
+
enforcement_fragment = _build_enforcement_system_prompt() if _caller_uses_global_discipline(caller) else ""
|
|
1144
1189
|
if enforcement_fragment:
|
|
1145
1190
|
if append_system_prompt:
|
|
1146
1191
|
append_system_prompt = append_system_prompt + "\n\n" + enforcement_fragment
|
|
@@ -1239,19 +1284,29 @@ def run_automation_prompt(
|
|
|
1239
1284
|
if allowed_tools:
|
|
1240
1285
|
cmd.extend(["--allowedTools", allowed_tools])
|
|
1241
1286
|
cmd.extend(extra_args)
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1287
|
+
if _caller_uses_global_discipline(caller):
|
|
1288
|
+
try:
|
|
1289
|
+
import sys as _sys
|
|
1290
|
+
if str(NEXO_HOME) not in _sys.path:
|
|
1291
|
+
_sys.path.insert(0, str(NEXO_HOME))
|
|
1292
|
+
from enforcement_engine import run_with_enforcement
|
|
1293
|
+
result = run_with_enforcement(
|
|
1294
|
+
cmd,
|
|
1295
|
+
prompt=prompt,
|
|
1296
|
+
cwd=str(cwd_path),
|
|
1297
|
+
env=run_env,
|
|
1298
|
+
timeout=timeout,
|
|
1299
|
+
)
|
|
1300
|
+
except ImportError:
|
|
1301
|
+
result = subprocess.run(
|
|
1302
|
+
cmd,
|
|
1303
|
+
cwd=str(cwd_path),
|
|
1304
|
+
capture_output=True,
|
|
1305
|
+
text=True,
|
|
1306
|
+
timeout=timeout,
|
|
1307
|
+
env=run_env,
|
|
1308
|
+
)
|
|
1309
|
+
else:
|
|
1255
1310
|
result = subprocess.run(
|
|
1256
1311
|
cmd,
|
|
1257
1312
|
cwd=str(cwd_path),
|
|
@@ -1264,6 +1319,7 @@ def run_automation_prompt(
|
|
|
1264
1319
|
result.stdout or "",
|
|
1265
1320
|
requested_output_format=requested_output_format,
|
|
1266
1321
|
)
|
|
1322
|
+
telemetry["automation_contract"] = automation_contract
|
|
1267
1323
|
recorded, record_error = _record_automation_run(
|
|
1268
1324
|
backend=selected_backend,
|
|
1269
1325
|
task_profile=task_profile,
|
|
@@ -1323,6 +1379,7 @@ def run_automation_prompt(
|
|
|
1323
1379
|
output_format=output_format,
|
|
1324
1380
|
append_system_prompt=append_system_prompt,
|
|
1325
1381
|
allowed_tools=allowed_tools,
|
|
1382
|
+
include_protocol_contract=_caller_uses_global_discipline(caller),
|
|
1326
1383
|
)
|
|
1327
1384
|
)
|
|
1328
1385
|
result = subprocess.run(
|
|
@@ -1340,6 +1397,7 @@ def run_automation_prompt(
|
|
|
1340
1397
|
final_stdout=stdout,
|
|
1341
1398
|
model=resolved_model,
|
|
1342
1399
|
)
|
|
1400
|
+
telemetry["automation_contract"] = automation_contract
|
|
1343
1401
|
recorded, record_error = _record_automation_run(
|
|
1344
1402
|
backend=selected_backend,
|
|
1345
1403
|
task_profile=task_profile,
|
|
@@ -1351,6 +1409,9 @@ def run_automation_prompt(
|
|
|
1351
1409
|
returncode=result.returncode,
|
|
1352
1410
|
duration_ms=int((time.perf_counter() - started_at) * 1000),
|
|
1353
1411
|
telemetry=telemetry,
|
|
1412
|
+
caller=caller,
|
|
1413
|
+
session_type="headless",
|
|
1414
|
+
resonance_tier=resonance_tier,
|
|
1354
1415
|
)
|
|
1355
1416
|
stderr = result.stderr or ""
|
|
1356
1417
|
if not recorded:
|
package/src/cli.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Entry points:
|
|
5
5
|
nexo chat [PATH]
|
|
6
|
+
nexo --version [--json]
|
|
6
7
|
nexo export [PATH] [--json]
|
|
7
8
|
nexo import-inspect PATH [--json]
|
|
8
9
|
nexo import PATH [--json]
|
|
@@ -233,17 +234,41 @@ def _version_sort_key(raw: str) -> tuple[tuple[int, ...], int, str]:
|
|
|
233
234
|
return (tuple(parts), 1 if not suffix else 0, suffix)
|
|
234
235
|
|
|
235
236
|
|
|
236
|
-
def
|
|
237
|
+
def _version_status_payload() -> dict:
|
|
237
238
|
installed = _get_version()
|
|
238
239
|
latest = _load_latest_version_cache()
|
|
240
|
+
latest_source = "cache" if latest else ""
|
|
239
241
|
if latest is None and _should_refresh_latest_version():
|
|
240
242
|
latest = _fetch_latest_version()
|
|
243
|
+
latest_source = "npm" if latest else ""
|
|
241
244
|
if latest and installed and _version_sort_key(latest) < _version_sort_key(installed):
|
|
242
245
|
latest = installed
|
|
246
|
+
latest_source = "installed"
|
|
243
247
|
try:
|
|
244
248
|
_save_latest_version_cache(installed)
|
|
245
249
|
except Exception:
|
|
246
250
|
pass
|
|
251
|
+
has_update = bool(
|
|
252
|
+
latest
|
|
253
|
+
and installed
|
|
254
|
+
and _version_sort_key(latest) > _version_sort_key(installed)
|
|
255
|
+
)
|
|
256
|
+
return {
|
|
257
|
+
"ok": True,
|
|
258
|
+
"name": "nexo",
|
|
259
|
+
"package": LATEST_NPM_PACKAGE,
|
|
260
|
+
"installed": installed,
|
|
261
|
+
"latest": latest or "",
|
|
262
|
+
"hasUpdate": has_update,
|
|
263
|
+
"unknown": not bool(installed and latest),
|
|
264
|
+
"latestSource": latest_source,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _version_status_line(payload: dict | None = None) -> str:
|
|
269
|
+
payload = payload or _version_status_payload()
|
|
270
|
+
installed = str(payload.get("installed") or "?").strip()
|
|
271
|
+
latest = str(payload.get("latest") or "").strip()
|
|
247
272
|
if latest:
|
|
248
273
|
return f"NEXO Latest: v{latest} | Installed: v{installed}"
|
|
249
274
|
return f"NEXO Installed: v{installed}"
|
|
@@ -2823,6 +2848,7 @@ def main():
|
|
|
2823
2848
|
parser = argparse.ArgumentParser(prog="nexo", description="NEXO Runtime CLI", add_help=False)
|
|
2824
2849
|
parser.add_argument("-h", "--help", action="store_true", help="Show help")
|
|
2825
2850
|
parser.add_argument("-v", "--version", action="store_true", help="Show version")
|
|
2851
|
+
parser.add_argument("--json", action="store_true", help="JSON output for --version")
|
|
2826
2852
|
sub = parser.add_subparsers(dest="command")
|
|
2827
2853
|
|
|
2828
2854
|
# -- email (Plan F1 — interactive wizard for email accounts) --
|
|
@@ -3408,7 +3434,12 @@ def main():
|
|
|
3408
3434
|
_print_help()
|
|
3409
3435
|
return 0
|
|
3410
3436
|
if args.version:
|
|
3411
|
-
|
|
3437
|
+
payload = _version_status_payload()
|
|
3438
|
+
if args.json:
|
|
3439
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
3440
|
+
else:
|
|
3441
|
+
print(f"nexo v{payload.get('installed') or _get_version()}")
|
|
3442
|
+
print(_version_status_line(payload))
|
|
3412
3443
|
return 0
|
|
3413
3444
|
|
|
3414
3445
|
if args.command == "email":
|
|
@@ -55,6 +55,15 @@ OPTIONALS_FILE = paths.config_dir() / "optionals.json"
|
|
|
55
55
|
SCHEDULE_FILE = paths.config_dir() / "schedule.json"
|
|
56
56
|
PACKAGE_JSON = NEXO_CODE / "package.json"
|
|
57
57
|
CHANGELOG_FILE = NEXO_CODE / "CHANGELOG.md"
|
|
58
|
+
CORE_AUTOMATION_CALLERS_BY_CRON = {
|
|
59
|
+
"catchup": ("catchup/morning",),
|
|
60
|
+
"deep-sleep": ("deep-sleep/extract", "deep-sleep/synthesize"),
|
|
61
|
+
"email-monitor": ("email_monitor",),
|
|
62
|
+
"evolution": ("evolution/run",),
|
|
63
|
+
"followup-runner": ("followup_runner",),
|
|
64
|
+
"morning-agent": ("morning_agent",),
|
|
65
|
+
"sleep": ("sleep/nightly",),
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
|
|
60
69
|
def _evolution_objective_payload() -> dict:
|
|
@@ -3676,6 +3685,154 @@ def check_automation_telemetry(days: int = 7) -> DoctorCheck:
|
|
|
3676
3685
|
)
|
|
3677
3686
|
|
|
3678
3687
|
|
|
3688
|
+
def check_automation_caller_coverage(days: int = 7) -> DoctorCheck:
|
|
3689
|
+
"""Compare core cron activity with caller-attributed automation rows.
|
|
3690
|
+
|
|
3691
|
+
This is a support signal, not proof of failure: a cron can legitimately
|
|
3692
|
+
have no model call when there is no work. It still catches the regression
|
|
3693
|
+
class where a runner branch records rows without caller/session context.
|
|
3694
|
+
"""
|
|
3695
|
+
db_path = paths.db_path()
|
|
3696
|
+
if not db_path.is_file():
|
|
3697
|
+
return DoctorCheck(
|
|
3698
|
+
id="runtime.automation_caller_coverage",
|
|
3699
|
+
tier="runtime",
|
|
3700
|
+
status="degraded",
|
|
3701
|
+
severity="warn",
|
|
3702
|
+
summary="Automation caller coverage DB is missing",
|
|
3703
|
+
evidence=[str(db_path)],
|
|
3704
|
+
repair_plan=["Run NEXO once so migrations create the shared runtime DB"],
|
|
3705
|
+
escalation_prompt="Support cannot compare core cron activity with automation caller traces.",
|
|
3706
|
+
)
|
|
3707
|
+
|
|
3708
|
+
expected_crons = tuple(CORE_AUTOMATION_CALLERS_BY_CRON)
|
|
3709
|
+
placeholders = ",".join("?" for _ in expected_crons)
|
|
3710
|
+
|
|
3711
|
+
try:
|
|
3712
|
+
conn = sqlite3.connect(str(db_path), timeout=2)
|
|
3713
|
+
conn.row_factory = sqlite3.Row
|
|
3714
|
+
try:
|
|
3715
|
+
tables = {
|
|
3716
|
+
row["name"]
|
|
3717
|
+
for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
|
|
3718
|
+
}
|
|
3719
|
+
missing_tables = [name for name in ("cron_runs", "automation_runs") if name not in tables]
|
|
3720
|
+
if missing_tables:
|
|
3721
|
+
return DoctorCheck(
|
|
3722
|
+
id="runtime.automation_caller_coverage",
|
|
3723
|
+
tier="runtime",
|
|
3724
|
+
status="degraded",
|
|
3725
|
+
severity="warn",
|
|
3726
|
+
summary="Automation caller coverage schema is incomplete",
|
|
3727
|
+
evidence=[f"missing_tables={','.join(missing_tables)}"],
|
|
3728
|
+
repair_plan=["Run NEXO migrations before trusting automation caller coverage"],
|
|
3729
|
+
escalation_prompt="Core automation activity cannot be matched to caller-attributed model runs.",
|
|
3730
|
+
)
|
|
3731
|
+
cron_rows = conn.execute(
|
|
3732
|
+
f"""
|
|
3733
|
+
SELECT cron_id, COUNT(*) AS runs, MAX(started_at) AS last_started
|
|
3734
|
+
FROM cron_runs
|
|
3735
|
+
WHERE started_at >= datetime('now', ?)
|
|
3736
|
+
AND cron_id IN ({placeholders})
|
|
3737
|
+
GROUP BY cron_id
|
|
3738
|
+
ORDER BY cron_id
|
|
3739
|
+
""",
|
|
3740
|
+
(f"-{days} days", *expected_crons),
|
|
3741
|
+
).fetchall()
|
|
3742
|
+
caller_rows = conn.execute(
|
|
3743
|
+
"""
|
|
3744
|
+
SELECT caller, COUNT(*) AS runs,
|
|
3745
|
+
MAX(COALESCE(started_at, created_at)) AS last_seen
|
|
3746
|
+
FROM automation_runs
|
|
3747
|
+
WHERE COALESCE(started_at, created_at) >= datetime('now', ?)
|
|
3748
|
+
GROUP BY caller
|
|
3749
|
+
""",
|
|
3750
|
+
(f"-{days} days",),
|
|
3751
|
+
).fetchall()
|
|
3752
|
+
finally:
|
|
3753
|
+
conn.close()
|
|
3754
|
+
except Exception as exc:
|
|
3755
|
+
return DoctorCheck(
|
|
3756
|
+
id="runtime.automation_caller_coverage",
|
|
3757
|
+
tier="runtime",
|
|
3758
|
+
status="degraded",
|
|
3759
|
+
severity="warn",
|
|
3760
|
+
summary="Automation caller coverage is unreadable",
|
|
3761
|
+
evidence=[str(exc)],
|
|
3762
|
+
repair_plan=["Inspect runtime DB cron_runs and automation_runs tables"],
|
|
3763
|
+
escalation_prompt="Support cannot verify whether core automations are leaving caller traces.",
|
|
3764
|
+
)
|
|
3765
|
+
|
|
3766
|
+
caller_counts = {str(row["caller"] or ""): int(row["runs"] or 0) for row in caller_rows}
|
|
3767
|
+
caller_last_seen = {str(row["caller"] or ""): str(row["last_seen"] or "") for row in caller_rows}
|
|
3768
|
+
cron_rows = list(cron_rows)
|
|
3769
|
+
|
|
3770
|
+
evidence = [f"window={days}d", f"core_crons_seen={len(cron_rows)}"]
|
|
3771
|
+
missing_matches: list[str] = []
|
|
3772
|
+
for row in cron_rows:
|
|
3773
|
+
cron_id = str(row["cron_id"] or "")
|
|
3774
|
+
expected_callers = CORE_AUTOMATION_CALLERS_BY_CRON.get(cron_id, ())
|
|
3775
|
+
matched = [caller for caller in expected_callers if caller_counts.get(caller, 0) > 0]
|
|
3776
|
+
if matched:
|
|
3777
|
+
evidence.append(
|
|
3778
|
+
f"{cron_id}: cron_runs={int(row['runs'] or 0)} caller_runs="
|
|
3779
|
+
+ ",".join(f"{caller}:{caller_counts[caller]}" for caller in matched)
|
|
3780
|
+
)
|
|
3781
|
+
else:
|
|
3782
|
+
expected = "|".join(expected_callers)
|
|
3783
|
+
missing_matches.append(cron_id)
|
|
3784
|
+
evidence.append(
|
|
3785
|
+
f"{cron_id}: cron_runs={int(row['runs'] or 0)} no_matching_caller={expected} "
|
|
3786
|
+
f"last_started={row['last_started']}"
|
|
3787
|
+
)
|
|
3788
|
+
|
|
3789
|
+
empty_caller_runs = caller_counts.get("", 0)
|
|
3790
|
+
if empty_caller_runs:
|
|
3791
|
+
evidence.append(f"empty_caller_runs={empty_caller_runs}")
|
|
3792
|
+
for caller in sorted(caller_counts):
|
|
3793
|
+
if caller:
|
|
3794
|
+
evidence.append(f"caller_seen={caller}:{caller_counts[caller]} last={caller_last_seen.get(caller, '')}")
|
|
3795
|
+
|
|
3796
|
+
if not cron_rows:
|
|
3797
|
+
return DoctorCheck(
|
|
3798
|
+
id="runtime.automation_caller_coverage",
|
|
3799
|
+
tier="runtime",
|
|
3800
|
+
status="healthy",
|
|
3801
|
+
severity="info",
|
|
3802
|
+
summary="No recent core automation cron activity to match",
|
|
3803
|
+
evidence=evidence,
|
|
3804
|
+
repair_plan=[],
|
|
3805
|
+
escalation_prompt="",
|
|
3806
|
+
)
|
|
3807
|
+
|
|
3808
|
+
status = "healthy"
|
|
3809
|
+
severity = "info"
|
|
3810
|
+
repair_plan: list[str] = []
|
|
3811
|
+
if empty_caller_runs or missing_matches:
|
|
3812
|
+
status = "degraded"
|
|
3813
|
+
severity = "warn"
|
|
3814
|
+
repair_plan.append(
|
|
3815
|
+
"If the cron had work, inspect run_automation_prompt caller attribution; if it was a no-op tick, no user action is needed"
|
|
3816
|
+
)
|
|
3817
|
+
|
|
3818
|
+
return DoctorCheck(
|
|
3819
|
+
id="runtime.automation_caller_coverage",
|
|
3820
|
+
tier="runtime",
|
|
3821
|
+
status=status,
|
|
3822
|
+
severity=severity,
|
|
3823
|
+
summary=(
|
|
3824
|
+
"Core automation caller coverage looks healthy"
|
|
3825
|
+
if status == "healthy"
|
|
3826
|
+
else "Core automation caller coverage needs review"
|
|
3827
|
+
),
|
|
3828
|
+
evidence=evidence,
|
|
3829
|
+
repair_plan=repair_plan,
|
|
3830
|
+
escalation_prompt=(
|
|
3831
|
+
"A core automation cron ran without a matching caller-attributed automation row in the selected window."
|
|
3832
|
+
) if status != "healthy" else "",
|
|
3833
|
+
)
|
|
3834
|
+
|
|
3835
|
+
|
|
3679
3836
|
def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
3680
3837
|
"""Run all runtime-tier checks. Read-only by default."""
|
|
3681
3838
|
return [
|
|
@@ -3695,6 +3852,7 @@ def run_runtime_checks(fix: bool = False) -> list[DoctorCheck]:
|
|
|
3695
3852
|
safe_check(check_client_assumption_regressions),
|
|
3696
3853
|
safe_check(check_protocol_compliance),
|
|
3697
3854
|
safe_check(check_automation_telemetry),
|
|
3855
|
+
safe_check(check_automation_caller_coverage),
|
|
3698
3856
|
safe_check(check_state_watchers),
|
|
3699
3857
|
safe_check(check_release_artifact_sync),
|
|
3700
3858
|
safe_check(check_release_trace_hygiene),
|
|
@@ -190,7 +190,7 @@ def smart_check(action_description: str, context: str = "") -> dict:
|
|
|
190
190
|
timeout=300,
|
|
191
191
|
output_format="text",
|
|
192
192
|
append_system_prompt=render_core_prompt("json-object-only"),
|
|
193
|
-
allowed_tools="Read,
|
|
193
|
+
allowed_tools="Read,Glob,Grep",
|
|
194
194
|
)
|
|
195
195
|
if result.returncode == 0:
|
|
196
196
|
parsed = _extract_json(result.stdout)
|