nexo-brain 2.2.0 → 2.3.1
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 +5 -5
- package/package.json +6 -3
- package/src/auto_update.py +26 -0
- package/src/crons/manifest.json +6 -13
- package/src/crons/sync.py +150 -6
- package/src/db/__init__.py +13 -0
- package/src/db/_core.py +1 -0
- package/src/db/_cron_runs.py +74 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +41 -6
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +64 -0
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +515 -0
- package/src/hooks/session-stop.sh +13 -101
- package/src/plugin_loader.py +1 -0
- package/src/plugins/episodic_memory.py +5 -3
- package/src/plugins/schedule.py +212 -0
- package/src/plugins/skills.py +264 -0
- package/src/plugins/update.py +1 -0
- package/src/scripts/deep-sleep/apply_findings.py +111 -8
- package/src/scripts/deep-sleep/collect.py +34 -11
- package/src/scripts/deep-sleep/extract-prompt.md +38 -0
- package/src/scripts/deep-sleep/extract.py +81 -8
- package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
- package/src/scripts/deep-sleep/synthesize.py +4 -1
- package/src/scripts/nexo-catchup.py +65 -29
- package/src/scripts/nexo-cron-wrapper.sh +53 -0
- package/src/scripts/nexo-daily-self-audit.py +4 -2
- package/src/scripts/nexo-deep-sleep.sh +66 -77
- package/src/scripts/nexo-evolution-run.py +13 -0
- package/src/scripts/nexo-learning-housekeep.py +157 -1
- package/src/scripts/nexo-learning-validator.py +19 -0
- package/src/scripts/nexo-postmortem-consolidator.py +3 -2
- package/src/scripts/nexo-sleep.py +16 -11
- package/src/scripts/nexo-synthesis.py +46 -3
- package/src/scripts/nexo-watchdog.sh +91 -30
- package/src/server.py +6 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-github-monitor.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
"""
|
|
3
4
|
Deep Sleep v2 -- Phase 1: Collect all context for overnight analysis.
|
|
4
5
|
|
|
@@ -116,8 +117,15 @@ def extract_session(jsonl_path: Path) -> dict | None:
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
|
|
119
|
-
def
|
|
120
|
-
"""Collect all sessions modified
|
|
120
|
+
def collect_transcripts_since(since_iso: str, until_iso: str = "") -> list[dict]:
|
|
121
|
+
"""Collect all sessions modified after `since_iso` (exclusive) up to `until_iso` (inclusive).
|
|
122
|
+
|
|
123
|
+
Uses a watermark approach: deep sleep tracks the last processed timestamp
|
|
124
|
+
so nothing is missed regardless of when sessions happen (day, night, etc.).
|
|
125
|
+
"""
|
|
126
|
+
since_dt = datetime.fromisoformat(since_iso)
|
|
127
|
+
until_dt = datetime.fromisoformat(until_iso) if until_iso else datetime.now()
|
|
128
|
+
|
|
121
129
|
sessions = []
|
|
122
130
|
for sdir in find_session_dirs():
|
|
123
131
|
for f in sdir.glob("*.jsonl"):
|
|
@@ -125,7 +133,7 @@ def collect_transcripts(target_date: str) -> list[dict]:
|
|
|
125
133
|
mtime = datetime.fromtimestamp(f.stat().st_mtime)
|
|
126
134
|
except OSError:
|
|
127
135
|
continue
|
|
128
|
-
if mtime
|
|
136
|
+
if since_dt < mtime <= until_dt:
|
|
129
137
|
session = extract_session(f)
|
|
130
138
|
if session:
|
|
131
139
|
session["modified"] = mtime.isoformat()
|
|
@@ -339,25 +347,40 @@ def format_transcripts(sessions: list[dict]) -> str:
|
|
|
339
347
|
|
|
340
348
|
|
|
341
349
|
def main():
|
|
342
|
-
|
|
350
|
+
# Watermark-based collection: since_iso and until_iso passed by the wrapper script
|
|
351
|
+
# argv[1] = run_id (date label for output files)
|
|
352
|
+
# argv[2] = since_iso (exclusive lower bound, e.g. "2026-04-01T04:30:00")
|
|
353
|
+
# argv[3] = until_iso (inclusive upper bound, e.g. "2026-04-02T04:30:00") — optional, defaults to now
|
|
354
|
+
run_id = sys.argv[1] if len(sys.argv) > 1 else datetime.now().strftime("%Y-%m-%d")
|
|
355
|
+
since_iso = sys.argv[2] if len(sys.argv) > 2 else ""
|
|
356
|
+
until_iso = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
357
|
+
|
|
343
358
|
DEEP_SLEEP_DIR.mkdir(parents=True, exist_ok=True)
|
|
344
359
|
|
|
345
|
-
print(f"[collect] Phase 1: Collecting context
|
|
360
|
+
print(f"[collect] Phase 1: Collecting context (run_id={run_id})")
|
|
346
361
|
|
|
347
|
-
# 1. Transcripts
|
|
348
|
-
|
|
349
|
-
|
|
362
|
+
# 1. Transcripts — watermark-based
|
|
363
|
+
if since_iso:
|
|
364
|
+
print(f"[collect] Gathering transcripts since {since_iso}" + (f" until {until_iso}" if until_iso else ""))
|
|
365
|
+
sessions = collect_transcripts_since(since_iso, until_iso)
|
|
366
|
+
else:
|
|
367
|
+
# Fallback: collect everything from last 48h (safe catch-all)
|
|
368
|
+
fallback_since = (datetime.now() - timedelta(hours=48)).isoformat()
|
|
369
|
+
print(f"[collect] No watermark — collecting last 48h since {fallback_since}")
|
|
370
|
+
sessions = collect_transcripts_since(fallback_since)
|
|
350
371
|
print(f" Found {len(sessions)} sessions")
|
|
351
372
|
|
|
352
373
|
if not sessions:
|
|
353
|
-
print(f"[collect] No sessions found
|
|
354
|
-
output_file = DEEP_SLEEP_DIR / f"{
|
|
374
|
+
print(f"[collect] No new sessions found. Writing minimal context file.")
|
|
375
|
+
output_file = DEEP_SLEEP_DIR / f"{run_id}-context.txt"
|
|
355
376
|
output_file.write_text(
|
|
356
|
-
f"Deep Sleep Context for {
|
|
377
|
+
f"Deep Sleep Context for {run_id}\n\nNo sessions found.\n"
|
|
357
378
|
)
|
|
358
379
|
print(f"[collect] Output: {output_file}")
|
|
359
380
|
return
|
|
360
381
|
|
|
382
|
+
target_date = run_id # Keep variable name for downstream compat
|
|
383
|
+
|
|
361
384
|
# 2. Core DB data
|
|
362
385
|
print("[collect] Querying databases...")
|
|
363
386
|
followups = collect_followups()
|
|
@@ -58,6 +58,22 @@ Detect work that was started but not finished in this session:
|
|
|
58
58
|
- Investigations started but conclusions never reached
|
|
59
59
|
Only flag if the work was NOT captured in a followup or reminder.
|
|
60
60
|
|
|
61
|
+
### 9. Skill Candidates (Reusable Procedures)
|
|
62
|
+
Detect multi-step tasks that were completed successfully and could be reused:
|
|
63
|
+
- Tasks that required 3+ distinct steps to complete
|
|
64
|
+
- Tasks where the agent followed a clear sequence of actions
|
|
65
|
+
- Procedures that are likely to be repeated in the future
|
|
66
|
+
- Examples: deploying code, configuring a service, running an audit, setting up infrastructure
|
|
67
|
+
|
|
68
|
+
For each candidate, extract:
|
|
69
|
+
- The full step-by-step procedure (what was actually done, in order)
|
|
70
|
+
- Tags describing the domain (e.g., "shopify", "chrome", "deploy")
|
|
71
|
+
- Trigger phrases that would indicate this procedure is needed (e.g., "deploy extension", "push theme")
|
|
72
|
+
- Any gotchas or warnings discovered during execution
|
|
73
|
+
|
|
74
|
+
Only flag if the procedure was SUCCESSFUL (the task was completed without major failures).
|
|
75
|
+
Do NOT flag trivial tasks (single-step actions, simple file edits, quick lookups).
|
|
76
|
+
|
|
61
77
|
### 8. Productivity Patterns
|
|
62
78
|
Analyze how the session went in terms of efficiency:
|
|
63
79
|
- How many times did the agent need correction before getting it right?
|
|
@@ -195,6 +211,28 @@ Return ONLY valid JSON. No markdown code fences. No explanation text before or a
|
|
|
195
211
|
}
|
|
196
212
|
],
|
|
197
213
|
|
|
214
|
+
"skill_candidates": [
|
|
215
|
+
{
|
|
216
|
+
"name": "Short name for the procedure (e.g., Deploy Chrome Extension)",
|
|
217
|
+
"description": "What this procedure accomplishes (1-2 sentences)",
|
|
218
|
+
"steps": [
|
|
219
|
+
"Step 1: What was done first",
|
|
220
|
+
"Step 2: What was done next",
|
|
221
|
+
"Step 3: etc."
|
|
222
|
+
],
|
|
223
|
+
"tags": ["domain1", "domain2"],
|
|
224
|
+
"trigger_phrases": ["phrase that would trigger this", "another trigger"],
|
|
225
|
+
"gotchas": ["Warning or caveat discovered during execution"],
|
|
226
|
+
"evidence": {
|
|
227
|
+
"type": "transcript",
|
|
228
|
+
"session_id": "filename.jsonl",
|
|
229
|
+
"message_index": 10,
|
|
230
|
+
"quote": "Start of the multi-step task"
|
|
231
|
+
},
|
|
232
|
+
"confidence": 0.85
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
|
|
198
236
|
"productivity_score": {
|
|
199
237
|
"corrections_needed": 0,
|
|
200
238
|
"proactivity": "reactive|mixed|proactive",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
"""
|
|
3
4
|
Deep Sleep v2 -- Phase 2: Extract findings from each session using Claude CLI.
|
|
4
5
|
|
|
@@ -113,6 +114,14 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
|
|
|
113
114
|
try:
|
|
114
115
|
env = os.environ.copy()
|
|
115
116
|
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
117
|
+
env.pop("CLAUDECODE", None)
|
|
118
|
+
env.pop("CLAUDE_CODE", None)
|
|
119
|
+
|
|
120
|
+
JSON_SYSTEM_PROMPT = (
|
|
121
|
+
"You are a JSON-only analyst. Your ENTIRE response must be a single valid JSON object. "
|
|
122
|
+
"No text before it. No text after it. No markdown fences. No explanations. "
|
|
123
|
+
"If you want to summarize, put it inside the JSON fields. Start with { and end with }."
|
|
124
|
+
)
|
|
116
125
|
|
|
117
126
|
result = subprocess.run(
|
|
118
127
|
[
|
|
@@ -120,8 +129,9 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
|
|
|
120
129
|
"-p", prompt,
|
|
121
130
|
"--model", "opus",
|
|
122
131
|
"--output-format", "text",
|
|
132
|
+
"--append-system-prompt", JSON_SYSTEM_PROMPT,
|
|
123
133
|
"--allowedTools",
|
|
124
|
-
"Read,Grep,Bash
|
|
134
|
+
"Read,Grep,Bash"
|
|
125
135
|
],
|
|
126
136
|
capture_output=True,
|
|
127
137
|
text=True,
|
|
@@ -139,6 +149,28 @@ def analyze_session(session_id: str, date_dir: Path, shared_context_file: Path |
|
|
|
139
149
|
if not line.strip().startswith("Post-mortem") and line.strip()
|
|
140
150
|
)
|
|
141
151
|
parsed = extract_json_from_response(output)
|
|
152
|
+
|
|
153
|
+
# Fallback: if Claude returned text instead of JSON, ask a short conversion call
|
|
154
|
+
if not parsed and len(output.strip()) > 50:
|
|
155
|
+
print(f" Got text instead of JSON ({len(output)} chars). Converting...")
|
|
156
|
+
convert_prompt = (
|
|
157
|
+
f"Convert the following analysis into the exact JSON schema required. "
|
|
158
|
+
f"Return ONLY the JSON object, nothing else.\n\n"
|
|
159
|
+
f"Analysis:\n{output[:8000]}\n\n"
|
|
160
|
+
f"Required schema: session_id, findings[], emotional_timeline[], "
|
|
161
|
+
f"abandoned_projects[], skill_candidates[], productivity_score, protocol_summary"
|
|
162
|
+
)
|
|
163
|
+
convert_result = subprocess.run(
|
|
164
|
+
[claude_bin, "-p", convert_prompt, "--model", "sonnet",
|
|
165
|
+
"--output-format", "text",
|
|
166
|
+
"--append-system-prompt", JSON_SYSTEM_PROMPT],
|
|
167
|
+
capture_output=True, text=True, timeout=120, env=env
|
|
168
|
+
)
|
|
169
|
+
if convert_result.returncode == 0:
|
|
170
|
+
parsed = extract_json_from_response(convert_result.stdout)
|
|
171
|
+
if parsed:
|
|
172
|
+
print(f" Conversion succeeded")
|
|
173
|
+
|
|
142
174
|
if not parsed:
|
|
143
175
|
# Save raw output for debugging
|
|
144
176
|
debug_file = DEEP_SLEEP_DIR / f"debug-extract-{session_id[:20]}.txt"
|
|
@@ -207,32 +239,70 @@ def main():
|
|
|
207
239
|
print(f"[extract] Phase 2: Analyzing {len(session_files)} sessions for {target_date}")
|
|
208
240
|
print(f"[extract] Claude CLI: {claude_bin}")
|
|
209
241
|
|
|
242
|
+
# Checkpoint directory: one JSON per session, survives crashes
|
|
243
|
+
checkpoint_dir = date_dir / "checkpoints"
|
|
244
|
+
checkpoint_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
|
|
210
246
|
all_extractions = []
|
|
211
247
|
total_findings = 0
|
|
248
|
+
skipped = 0
|
|
249
|
+
MAX_RETRIES = 3
|
|
212
250
|
|
|
213
251
|
for i, session_id in enumerate(session_files):
|
|
252
|
+
sid_safe = session_id.replace(".jsonl", "")[:30]
|
|
253
|
+
checkpoint_file = checkpoint_dir / f"{sid_safe}.json"
|
|
254
|
+
|
|
255
|
+
# Resume: skip already-processed sessions
|
|
256
|
+
if checkpoint_file.exists():
|
|
257
|
+
try:
|
|
258
|
+
with open(checkpoint_file) as f:
|
|
259
|
+
cached = json.load(f)
|
|
260
|
+
findings_count = len(cached.get("findings", []))
|
|
261
|
+
total_findings += findings_count
|
|
262
|
+
all_extractions.append(cached)
|
|
263
|
+
skipped += 1
|
|
264
|
+
print(f"[extract] Session {i + 1}/{len(session_files)}: {session_id} (cached, {findings_count} findings)")
|
|
265
|
+
continue
|
|
266
|
+
except (json.JSONDecodeError, KeyError):
|
|
267
|
+
pass # Corrupted checkpoint, re-process
|
|
268
|
+
|
|
214
269
|
print(f"[extract] Session {i + 1}/{len(session_files)}: {session_id}")
|
|
215
270
|
|
|
216
|
-
|
|
271
|
+
# Retry loop
|
|
272
|
+
result = None
|
|
273
|
+
for attempt in range(1, MAX_RETRIES + 1):
|
|
274
|
+
result = analyze_session(session_id, date_dir, shared_context_file, claude_bin)
|
|
275
|
+
if result:
|
|
276
|
+
break
|
|
277
|
+
if attempt < MAX_RETRIES:
|
|
278
|
+
print(f" -> Attempt {attempt}/{MAX_RETRIES} failed, retrying...")
|
|
217
279
|
|
|
218
280
|
if result:
|
|
219
281
|
findings_count = len(result.get("findings", []))
|
|
220
282
|
total_findings += findings_count
|
|
221
283
|
all_extractions.append(result)
|
|
222
|
-
|
|
284
|
+
# Save checkpoint
|
|
285
|
+
with open(checkpoint_file, "w") as f:
|
|
286
|
+
json.dump(result, f, indent=2, ensure_ascii=False)
|
|
287
|
+
print(f" -> {findings_count} findings extracted (checkpointed)")
|
|
223
288
|
else:
|
|
224
|
-
print(f" ->
|
|
225
|
-
|
|
289
|
+
print(f" -> Failed after {MAX_RETRIES} attempts, marking as failed")
|
|
290
|
+
failed_entry = {
|
|
226
291
|
"session_id": session_id,
|
|
227
292
|
"findings": [],
|
|
228
|
-
"error": "Extraction failed"
|
|
229
|
-
}
|
|
293
|
+
"error": f"Extraction failed after {MAX_RETRIES} attempts"
|
|
294
|
+
}
|
|
295
|
+
all_extractions.append(failed_entry)
|
|
296
|
+
# Save failed checkpoint too (so we don't retry forever)
|
|
297
|
+
with open(checkpoint_file, "w") as f:
|
|
298
|
+
json.dump(failed_entry, f, indent=2, ensure_ascii=False)
|
|
230
299
|
|
|
231
300
|
# Merge into output
|
|
232
301
|
output = {
|
|
233
302
|
"date": target_date,
|
|
234
303
|
"sessions_analyzed": len(session_files),
|
|
235
304
|
"sessions_succeeded": len([e for e in all_extractions if "error" not in e]),
|
|
305
|
+
"sessions_cached": skipped,
|
|
236
306
|
"total_findings": total_findings,
|
|
237
307
|
"extractions": all_extractions
|
|
238
308
|
}
|
|
@@ -241,7 +311,10 @@ def main():
|
|
|
241
311
|
with open(output_file, "w") as f:
|
|
242
312
|
json.dump(output, f, indent=2, ensure_ascii=False)
|
|
243
313
|
|
|
244
|
-
|
|
314
|
+
if skipped:
|
|
315
|
+
print(f"\n[extract] Done. {total_findings} findings from {len(session_files)} sessions ({skipped} cached, {len(session_files) - skipped} new).")
|
|
316
|
+
else:
|
|
317
|
+
print(f"\n[extract] Done. {total_findings} findings from {len(session_files)} sessions.")
|
|
245
318
|
print(f"[extract] Output: {output_file}")
|
|
246
319
|
|
|
247
320
|
|
|
@@ -73,6 +73,19 @@ Consider ALL of these:
|
|
|
73
73
|
|
|
74
74
|
The score should feel fair. A day with 2 minor corrections and 10 tasks completed is still a good day (75+). A day with 1 catastrophic error might be a 40 even if everything else was fine.
|
|
75
75
|
|
|
76
|
+
### 9. Skill Extraction
|
|
77
|
+
Consolidate `skill_candidates` from all session extractions into publishable skills:
|
|
78
|
+
- Merge similar procedures from different sessions into a single skill
|
|
79
|
+
- Generalize: replace session-specific IDs, paths, or names with placeholders or descriptions
|
|
80
|
+
- Only include skills with confidence >= 0.7
|
|
81
|
+
- Check if a similar skill already exists (use `nexo_skill_match` if available) — if so, note it for merging instead of creating new
|
|
82
|
+
|
|
83
|
+
For each skill, generate:
|
|
84
|
+
- A unique ID starting with `SK-` (e.g., `SK-DEPLOY-CHROME-EXT`)
|
|
85
|
+
- Name, description, tags, trigger_patterns
|
|
86
|
+
- The full step-by-step procedure as the skill content
|
|
87
|
+
- Source session IDs for traceability
|
|
88
|
+
|
|
76
89
|
### 8. Consolidated Actions
|
|
77
90
|
Merge and deduplicate all findings into a final action list. Each action should have:
|
|
78
91
|
- `action_type`: `learning_add`, `followup_create`, `morning_briefing_item`
|
|
@@ -122,9 +135,24 @@ Return ONLY valid JSON. No markdown code fences. No explanation text.
|
|
|
122
135
|
}
|
|
123
136
|
],
|
|
124
137
|
|
|
138
|
+
"skills": [
|
|
139
|
+
{
|
|
140
|
+
"id": "SK-SHORT-ID",
|
|
141
|
+
"name": "Human readable name",
|
|
142
|
+
"description": "What this procedure does (1-2 sentences)",
|
|
143
|
+
"steps": ["Step 1", "Step 2", "Step 3"],
|
|
144
|
+
"tags": ["tag1", "tag2"],
|
|
145
|
+
"trigger_patterns": ["trigger phrase 1", "trigger phrase 2"],
|
|
146
|
+
"gotchas": ["Warning or caveat"],
|
|
147
|
+
"source_sessions": ["session1.jsonl"],
|
|
148
|
+
"confidence": 0.85,
|
|
149
|
+
"merge_with": null
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
|
|
125
153
|
"actions": [
|
|
126
154
|
{
|
|
127
|
-
"action_type": "learning_add|followup_create|morning_briefing_item",
|
|
155
|
+
"action_type": "learning_add|followup_create|skill_create|morning_briefing_item",
|
|
128
156
|
"action_class": "auto_apply|draft_for_morning",
|
|
129
157
|
"confidence": 0.9,
|
|
130
158
|
"impact": "low|medium|high",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
2
3
|
"""
|
|
3
4
|
Deep Sleep v2 -- Phase 3: Synthesize extractions into actionable findings.
|
|
4
5
|
|
|
@@ -115,6 +116,8 @@ def main():
|
|
|
115
116
|
try:
|
|
116
117
|
env = os.environ.copy()
|
|
117
118
|
env["NEXO_HEADLESS"] = "1" # Skip stop hook post-mortem
|
|
119
|
+
env.pop("CLAUDECODE", None)
|
|
120
|
+
env.pop("CLAUDE_CODE", None)
|
|
118
121
|
|
|
119
122
|
result = subprocess.run(
|
|
120
123
|
[
|
|
@@ -123,7 +126,7 @@ def main():
|
|
|
123
126
|
"--model", "opus",
|
|
124
127
|
"--output-format", "text",
|
|
125
128
|
"--allowedTools",
|
|
126
|
-
"Read,
|
|
129
|
+
"Read,Grep,Bash"
|
|
127
130
|
],
|
|
128
131
|
capture_output=True,
|
|
129
132
|
text=True,
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
NEXO Catch-Up — Runs at
|
|
3
|
+
NEXO Catch-Up — Runs at boot/wake to recover any missed scheduled tasks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Tasks are loaded dynamically from crons/manifest.json (single source of truth).
|
|
6
|
+
Only scheduled crons (with hour/minute) are recovered — interval-based crons
|
|
7
|
+
(immune, watchdog, auto-close) restart automatically via launchd/systemd.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
04:00 — sleep (session cleanup)
|
|
13
|
-
07:00 — self-audit (health checks + weekly cognitive GC on Sundays)
|
|
14
|
-
23:30 — postmortem (consolidation + sensory register)
|
|
15
|
-
|
|
16
|
-
Logic: For each task, check if its last successful run was before the
|
|
17
|
-
most recent scheduled time. If so, run it now.
|
|
9
|
+
Logic: For each scheduled task, check if its last successful run was before
|
|
10
|
+
the most recent scheduled time. If so, run it now. Only marks success on exit 0.
|
|
11
|
+
Uses cron/launchd weekday convention (0=Sunday) converted to Python (0=Monday).
|
|
18
12
|
"""
|
|
19
13
|
|
|
20
14
|
import json
|
|
@@ -52,6 +46,49 @@ def _resolve_python() -> str:
|
|
|
52
46
|
return sys.executable
|
|
53
47
|
|
|
54
48
|
NEXO_PYTHON = _resolve_python()
|
|
49
|
+
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
|
|
50
|
+
MANIFEST = NEXO_CODE / "crons" / "manifest.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _load_tasks_from_manifest() -> list[tuple]:
|
|
54
|
+
"""Read scheduled tasks from manifest.json — single source of truth.
|
|
55
|
+
|
|
56
|
+
Only includes crons with a schedule (hour/minute). Excludes interval-based
|
|
57
|
+
crons (immune, watchdog, auto-close) and run_at_load (catchup itself).
|
|
58
|
+
Returns: list of (name, hour, minute, python_or_bash, script, weekday)
|
|
59
|
+
"""
|
|
60
|
+
if not MANIFEST.exists():
|
|
61
|
+
log(f"WARNING: manifest not found at {MANIFEST}, using empty task list")
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
with open(MANIFEST) as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
|
|
67
|
+
tasks = []
|
|
68
|
+
for cron in data.get("crons", []):
|
|
69
|
+
schedule = cron.get("schedule")
|
|
70
|
+
if not schedule or "hour" not in schedule:
|
|
71
|
+
continue # Skip interval-based and run_at_load crons
|
|
72
|
+
if cron["id"] == "catchup":
|
|
73
|
+
continue # Don't catch up ourselves
|
|
74
|
+
|
|
75
|
+
script = cron["script"]
|
|
76
|
+
script_type = cron.get("type", "python")
|
|
77
|
+
interpreter = NEXO_PYTHON if script_type == "python" else "/bin/bash"
|
|
78
|
+
weekday = schedule.get("weekday")
|
|
79
|
+
|
|
80
|
+
tasks.append((
|
|
81
|
+
cron["id"],
|
|
82
|
+
schedule["hour"],
|
|
83
|
+
schedule["minute"],
|
|
84
|
+
interpreter,
|
|
85
|
+
Path(script).name,
|
|
86
|
+
weekday,
|
|
87
|
+
))
|
|
88
|
+
|
|
89
|
+
# Sort by hour, minute for correct execution order
|
|
90
|
+
tasks.sort(key=lambda t: (t[1], t[2]))
|
|
91
|
+
return tasks
|
|
55
92
|
|
|
56
93
|
|
|
57
94
|
def log(msg: str):
|
|
@@ -83,7 +120,11 @@ def last_scheduled_time(hour: int, minute: int, weekday: int = None) -> datetime
|
|
|
83
120
|
|
|
84
121
|
if weekday is not None:
|
|
85
122
|
# Weekly task — find the most recent matching weekday
|
|
86
|
-
|
|
123
|
+
# Manifest uses cron/launchd convention: 0=Sunday, 6=Saturday
|
|
124
|
+
# Python datetime.weekday() uses: 0=Monday, 6=Sunday
|
|
125
|
+
# Convert: manifest 0 (Sun) -> python 6, manifest 1 (Mon) -> python 0, etc.
|
|
126
|
+
py_weekday = (weekday - 1) % 7
|
|
127
|
+
days_since = (now.weekday() - py_weekday) % 7
|
|
87
128
|
target = now - timedelta(days=days_since)
|
|
88
129
|
target = target.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
|
89
130
|
if target > now:
|
|
@@ -130,13 +171,14 @@ def run_task(name: str, python: str, script: str, state: dict) -> bool:
|
|
|
130
171
|
)
|
|
131
172
|
if result.returncode == 0:
|
|
132
173
|
log(f" OK {name} (exit 0)")
|
|
174
|
+
state[name] = datetime.now().isoformat()
|
|
175
|
+
save_state(state)
|
|
176
|
+
return True
|
|
133
177
|
else:
|
|
134
|
-
log(f"
|
|
178
|
+
log(f" FAIL {name} (exit {result.returncode})")
|
|
135
179
|
if result.stderr:
|
|
136
180
|
log(f" stderr: {result.stderr[:300]}")
|
|
137
|
-
|
|
138
|
-
save_state(state)
|
|
139
|
-
return True
|
|
181
|
+
return False
|
|
140
182
|
except subprocess.TimeoutExpired:
|
|
141
183
|
log(f" TIMEOUT {name} (300s)")
|
|
142
184
|
return False
|
|
@@ -149,17 +191,8 @@ def main():
|
|
|
149
191
|
log("=== NEXO Catch-Up starting (boot/wake) ===")
|
|
150
192
|
state = load_state()
|
|
151
193
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
tasks = [
|
|
155
|
-
# (name, hour, minute, python, script, weekday)
|
|
156
|
-
("cognitive-decay", 3, 0, NEXO_PYTHON, "nexo-cognitive-decay.py", None),
|
|
157
|
-
("evolution", 3, 0, NEXO_PYTHON, "nexo-evolution-run.py", 6), # Sunday = 6
|
|
158
|
-
("sleep", 4, 0, NEXO_PYTHON, "nexo-sleep.py", None),
|
|
159
|
-
("self-audit", 7, 0, NEXO_PYTHON, "nexo-daily-self-audit.py", None),
|
|
160
|
-
("github-monitor", 8, 0, NEXO_PYTHON, "nexo-github-monitor.py", None),
|
|
161
|
-
("postmortem", 23, 30, NEXO_PYTHON, "nexo-postmortem-consolidator.py", None),
|
|
162
|
-
]
|
|
194
|
+
# Read tasks from manifest — single source of truth
|
|
195
|
+
tasks = _load_tasks_from_manifest()
|
|
163
196
|
|
|
164
197
|
ran = 0
|
|
165
198
|
skipped = 0
|
|
@@ -187,6 +220,9 @@ def _cli_post_catchup_assessment(ran: int, skipped: int, state: dict):
|
|
|
187
220
|
if not CLAUDE_CLI.exists():
|
|
188
221
|
log(f"Caught up {ran} tasks, {skipped} already current. (CLI unavailable for assessment)")
|
|
189
222
|
return
|
|
223
|
+
auth_check = subprocess.run(
|
|
224
|
+
[str(CLAUDE_CLI), "-p", "reply OK", "--output-format", "text"],
|
|
225
|
+
capture_output=True, text=True, timeout=30
|
|
190
226
|
)
|
|
191
227
|
if auth_check.returncode != 0:
|
|
192
228
|
# CLI not authenticated, skip gracefully
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# NEXO Cron Wrapper — Records execution in cron_runs table.
|
|
3
|
+
# Usage: nexo-cron-wrapper.sh <cron_id> <command...>
|
|
4
|
+
# Example: nexo-cron-wrapper.sh deep-sleep bash nexo-deep-sleep.sh
|
|
5
|
+
#
|
|
6
|
+
# Wraps any cron command to automatically record start/end/exit_code/summary.
|
|
7
|
+
# Used by sync.py when generating LaunchAgents from manifest.json.
|
|
8
|
+
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
CRON_ID="${1:?Usage: nexo-cron-wrapper.sh <cron_id> <command...>}"
|
|
12
|
+
shift
|
|
13
|
+
|
|
14
|
+
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
15
|
+
DB="$NEXO_HOME/data/nexo.db"
|
|
16
|
+
|
|
17
|
+
# Record start
|
|
18
|
+
RUN_ID=$(sqlite3 "$DB" "INSERT INTO cron_runs (cron_id) VALUES ('$CRON_ID'); SELECT last_insert_rowid();" 2>/dev/null)
|
|
19
|
+
|
|
20
|
+
if [ -z "$RUN_ID" ]; then
|
|
21
|
+
# DB not ready — run without tracking
|
|
22
|
+
exec "$@"
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Run the actual command, capture output
|
|
26
|
+
OUTPUT_FILE=$(mktemp)
|
|
27
|
+
"$@" > "$OUTPUT_FILE" 2>&1
|
|
28
|
+
EXIT_CODE=$?
|
|
29
|
+
|
|
30
|
+
# Extract summary (last meaningful line, max 500 chars)
|
|
31
|
+
SUMMARY=$(tail -5 "$OUTPUT_FILE" | grep -v "^$" | tail -1 | head -c 500 | sed "s/'/''/g")
|
|
32
|
+
|
|
33
|
+
# Extract error if failed
|
|
34
|
+
ERROR=""
|
|
35
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
36
|
+
ERROR=$(grep -i "error\|exception\|fail\|traceback" "$OUTPUT_FILE" | tail -1 | head -c 500 | sed "s/'/''/g")
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Record end
|
|
40
|
+
sqlite3 "$DB" "
|
|
41
|
+
UPDATE cron_runs SET
|
|
42
|
+
ended_at = datetime('now'),
|
|
43
|
+
exit_code = $EXIT_CODE,
|
|
44
|
+
summary = '$SUMMARY',
|
|
45
|
+
error = '$ERROR',
|
|
46
|
+
duration_secs = ROUND((julianday(datetime('now')) - julianday(started_at)) * 86400, 1)
|
|
47
|
+
WHERE id = $RUN_ID;
|
|
48
|
+
" 2>/dev/null
|
|
49
|
+
|
|
50
|
+
# Clean output
|
|
51
|
+
rm -f "$OUTPUT_FILE"
|
|
52
|
+
|
|
53
|
+
exit $EXIT_CODE
|
|
@@ -532,8 +532,10 @@ def main():
|
|
|
532
532
|
"counts": {"error": errors, "warn": warns, "info": infos}
|
|
533
533
|
}, indent=2))
|
|
534
534
|
|
|
535
|
-
# Stage B: CLI interpretation
|
|
536
|
-
interpret_findings(findings)
|
|
535
|
+
# Stage B: CLI interpretation (graceful fallback if CLI unavailable)
|
|
536
|
+
cli_ok = interpret_findings(findings)
|
|
537
|
+
if not cli_ok:
|
|
538
|
+
log("Stage B: CLI unavailable or failed. Stage A results saved to self-audit-summary.json.")
|
|
537
539
|
|
|
538
540
|
# Register for catch-up
|
|
539
541
|
try:
|