nexo-brain 7.6.0 → 7.7.0

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
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,9 @@
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.6.0` is the current packaged-runtime line. Minor release that closes the drift between `tool-enforcement-map.json` v2.2 and the two enforcement engines (Brain Python + Desktop JS), and moves several default rule modes toward the obedience target requested by the constructor-guardian-90 checklist. Brain now dispatches `before_tool`, `on_event`, and `conditional` alongside the previously-handled types before v7.6 Brain silently ignored these three despite the map declaring them, so Brain users got the weaker contract. `after_tool` dependencies now satisfy per-instance (monotonic `_tool_instance_counter`) instead of once-per-session, which was the specific bug the checklist flagged. Map v2.2 tightens two triggers: `nexo_learning_add` grace_messages 3 0 (corrections produce the learning in the same turn) and `nexo_task_open` conditional threshold 10 4 with level `should must` and an explicit `inject_prompt`. `guardian_default.json` v1.4.0 raises `R15_project_context`, `R17_promise_debt`, `R22_personal_script` and `R_CATALOG_before_artifact_create` from soft to hard, and moves `R34_identity_coherence` from shadow to soft so identity denials surface a visible reminder instead of silent telemetry. A new contract test (`tests/test_v76_map_parity.py`) pins six invariants including Brain↔Desktop dispatch parity and per-instance satisfaction so a future refactor cannot quietly re-open the drift. Companion release: NEXO Desktop v0.26.0 mirrors the engine fixes + default hardening.
21
+ Version `7.7.0` is the current packaged-runtime line. Minor release that closes the six gaps left partial after v7.6.0's constructor-guardian-90 pass 1. Gap 1: an autonomous detector now raises `multi_step_task_detected` after three recent Edit/Write/Task calls without a prior `nexo_skill_match` (v7.6 dispatched the event but nothing raised it). Gap 2: the R16 classifier prompt recognises the full done-claim vocabulary (sent / delivered / published / deployed / released / fixed / resolved / merged / pushed plus Spanish equivalents) so the on_event trigger `done_claimed_with_open_task` fires beyond the word "done". Gap 3: R_CATALOG extends to plain `Edit` / `Write` into artefact-bearing paths (skills/, plugins/, personal scripts, `templates/core-prompts/`) and grows its discovery set to include `nexo_personal_scripts_list` + `nexo_plugin_list`. Gap 4: new `R_PRIMITIVE_CHOICE` runtime rule gates Edit/Write of a brand-new artefact without a recent primitive-choice probe (SK-CREATE-NEXO-PRIMITIVE). Gap 5: `guardian_default.json` v1.5.0 raises `R11_plugin_load_pre_inventory` from soft to hard. Gap 6: new `tests/test_v77_enforcement_gaps.py` pins 12 invariants across all six rails. Pytest 2070 passing (+14 vs v7.6.0). Companion release: NEXO Desktop v0.27.0 mirrors the guardian default bumps.
22
+
23
+ Previously in `7.6.0`: minor release that closed the drift between `tool-enforcement-map.json` v2.2 and the two enforcement engines (Brain Python + Desktop JS), added per-instance `after_tool` satisfaction, tightened `learning_add` grace to 0 and `task_open` threshold to 4/must, hardened R15/R17/R22/R_CATALOG from soft to hard, and raised R34 from shadow to soft.
22
24
 
23
25
  Previously in `7.5.0`: minor release that promoted `nexo_lifecycle_event` from ledger + reconciliation authority to **canonical authority of session-end**. Brain now owns the prompt, the sequence, and the timing of diary+stop; Desktop v0.25.0 (closed-source companion) is the conduit that executes Brain's plan against the live Claude process. The new 2-call contract — `nexo_lifecycle_event` returns a versioned `canonical_plan` (resume_session → inject_prompt → stop_session, with stable ids and per-action timeouts) and `nexo_lifecycle_complete_canonical` confirms execution with a per-action results array — replaces polling with explicit acknowledgement. `canonical_plan_id` is deterministic: `sha256(event_id + "|v" + plan_version)[:24]`, so retries reuse the same id. Migration m52 extends `lifecycle_events` with six `canonical_*` columns plus an index; pre-v7.5 rows simply carry NULL. `session_diary` is the dedupe key on re-delivery: if Desktop crashes between executing the inject and sending the complete call, the next `nexo_lifecycle_event` for the same `event_id` checks for a diary written after `canonical_dispatched_at`; if one exists, Brain short-circuits to `already_processed` and refuses to re-dispatch. The seven explicit `delivery_status` values (`accepted`, `processed`, `canonical_pending`, `canonical_done`, `already_processed`, `retryable_error`, `rejected`) give the pipeline a diffable state machine. `switch` and `window-close` stay observational (no plan ever issued, even with a live `session_id`). `nexo lifecycle record` now returns exit code 0 for `canonical_pending`; older wrappers that treated it as an error are incompatible with v7.5. MCP tool count: 262 → 263.
24
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.6.0",
3
+ "version": "7.7.0",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain \u2014 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",
@@ -80,6 +80,11 @@ try:
80
80
  except ImportError: # pragma: no cover
81
81
  _r_catalog_should = None # type: ignore
82
82
 
83
+ try:
84
+ from r_primitive_choice import should_inject_r_primitive as _r_primitive_should
85
+ except ImportError: # pragma: no cover
86
+ _r_primitive_should = None # type: ignore
87
+
83
88
  try:
84
89
  from r34_identity_coherence import should_inject_r34 as _r34_should
85
90
  except ImportError: # pragma: no cover
@@ -422,6 +427,9 @@ class HeadlessEnforcer:
422
427
  # on_event grace windows — per (event_name) counter of messages
423
428
  # since event fired without the required tool being called.
424
429
  self._on_event_pending: dict[str, dict] = {}
430
+ # v7.7 Gap 1: latch so multi_step_task_detected fires at most once
431
+ # per task cycle. Cleared on skill_match OR task_close.
432
+ self._multi_step_event_fired: bool = False
425
433
 
426
434
  if self.map:
427
435
  self._build_indexes()
@@ -650,6 +658,14 @@ class HeadlessEnforcer:
650
658
  self._r14_correction_seen_for_turn = True
651
659
  _logger.info("[R14 %s] correction detected; window opened for %d tool calls",
652
660
  mode.upper(), self._r14_window_remaining)
661
+ # v7.7 Gap 7.2 — wire on_event so the map's
662
+ # `user_correction_without_learning` rule fires in the live
663
+ # stream. grace_messages was set to 0 in map v2.2 so the
664
+ # learning reminder must surface the same turn.
665
+ try:
666
+ self.raise_event("user_correction_without_learning", {"text_hash": hash(text or "")})
667
+ except Exception:
668
+ pass # telemetry-style; never crash R14 detection
653
669
 
654
670
  def _run_session_end_detection(self, text: str, *, detector=None) -> bool:
655
671
  if not self._on_end:
@@ -848,6 +864,13 @@ class HeadlessEnforcer:
848
864
  return
849
865
  self._enqueue(_R16_PROMPT, "r16:declared-done-without-close", rule_id="R16_declared_done")
850
866
  _logger.info("[R16 %s] enqueued declared-done reminder", mode.upper())
867
+ # v7.7 Gap 7.2 — fire the on_event rule wired to task_close so
868
+ # the map's `done_claimed_with_open_task` trigger actually runs
869
+ # from the live stream, not only via test harnesses.
870
+ try:
871
+ self.raise_event("done_claimed_with_open_task", {"source": "R16"})
872
+ except Exception:
873
+ pass
851
874
 
852
875
  def _r25_context(self) -> tuple[set[str], list[str]]:
853
876
  """Resolve the (read_only_hosts, destructive_patterns) pair from
@@ -1753,8 +1776,15 @@ class HeadlessEnforcer:
1753
1776
  self._enqueue(prompt, decision["tag"], rule_id="R22_personal_script")
1754
1777
  _logger.info("[R22 %s] enqueued path=%s missing=%s", mode.upper(), decision["path"], decision["missing"])
1755
1778
 
1756
- def _check_r_catalog(self, tool_name: str):
1757
- """R-CATALOG (Plan Consolidado 0.X.2) — pre-create discovery probe."""
1779
+ def _check_r_catalog(self, tool_name: str, files: list[str] | None = None):
1780
+ """R-CATALOG — pre-create discovery probe.
1781
+
1782
+ v7.7 Gap 3: the trigger set is now {nexo_*_create/_open/_add}
1783
+ UNION {Edit / Write into artefact-bearing paths}. The caller
1784
+ passes the extracted file list so plain Edit/Write materialising
1785
+ a skill / plugin / script without going through a dedicated MCP
1786
+ tool still triggers the probe.
1787
+ """
1758
1788
  if _r_catalog_should is None:
1759
1789
  return
1760
1790
  mode = self._guardian_rule_mode("R_CATALOG_before_artifact_create")
@@ -1768,7 +1798,7 @@ class HeadlessEnforcer:
1768
1798
  r.tool for r in self.recent_tool_records[:-1]
1769
1799
  if (now - getattr(r, "ts", now)) <= window
1770
1800
  ]
1771
- should, prompt = _r_catalog_should(tool_name, recent_tool_names=names)
1801
+ should, prompt = _r_catalog_should(tool_name, recent_tool_names=names, files=files or [])
1772
1802
  if not should:
1773
1803
  return
1774
1804
  if mode == "shadow":
@@ -1777,6 +1807,45 @@ class HeadlessEnforcer:
1777
1807
  self._enqueue(prompt, f"R_CATALOG:{tool_name}", rule_id="R_CATALOG_before_artifact_create")
1778
1808
  _logger.info("[R_CATALOG %s] enqueued tool=%s", mode.upper(), tool_name)
1779
1809
 
1810
+ def _check_r_primitive_choice(self, tool_name: str, files: list[str] | None):
1811
+ """R_PRIMITIVE_CHOICE (v7.7 Gap 4) — SK-CREATE-NEXO-PRIMITIVE gate.
1812
+
1813
+ Flags Edit/Write of a NEW artefact file without a recent primitive-
1814
+ choice probe. Does not duplicate R_CATALOG: R_CATALOG fires on
1815
+ every artefact-path write without inventory consultation, while
1816
+ this rule fires only when the file is genuinely new (no prior
1817
+ Read / Grep / Edit on the same path).
1818
+ """
1819
+ if _r_primitive_should is None:
1820
+ return
1821
+ mode = self._guardian_rule_mode("R_PRIMITIVE_CHOICE")
1822
+ if mode == "off":
1823
+ return
1824
+ window = 120.0
1825
+ now = time.time()
1826
+ names = [
1827
+ r.tool for r in self.recent_tool_records[:-1]
1828
+ if (now - getattr(r, "ts", now)) <= window
1829
+ ]
1830
+ records = [r for r in self.recent_tool_records[:-1]]
1831
+ should, prompt = _r_primitive_should(
1832
+ tool_name,
1833
+ files=files or [],
1834
+ recent_tool_names=names,
1835
+ recent_tool_records=records,
1836
+ )
1837
+ if not should:
1838
+ return
1839
+ if mode == "shadow":
1840
+ _logger.info("[R_PRIMITIVE_CHOICE SHADOW] would inject for %s", tool_name)
1841
+ return
1842
+ self._enqueue(
1843
+ prompt,
1844
+ f"R_PRIMITIVE_CHOICE:{tool_name}",
1845
+ rule_id="R_PRIMITIVE_CHOICE",
1846
+ )
1847
+ _logger.info("[R_PRIMITIVE_CHOICE %s] enqueued tool=%s", mode.upper(), tool_name)
1848
+
1780
1849
  def _check_r18(self, tool_name: str, tool_input):
1781
1850
  """R18 — suggest followup_complete on closure-class actions."""
1782
1851
  if _r18_should is None or _r18_format is None:
@@ -1934,6 +2003,29 @@ class HeadlessEnforcer:
1934
2003
  # open tool so the next task cycle re-opens the obligation.
1935
2004
  if name == "nexo_task_close":
1936
2005
  self.reset_task_cycle("nexo_task_open")
2006
+
2007
+ # v7.7 Gap 1 — autonomous detector for multi_step_task_detected.
2008
+ # The event was dispatched by the map but nothing ever raised it.
2009
+ # Heuristic: three or more edit/execute/delegate calls within the
2010
+ # recent window (Edit/Write/Task/Bash-with-write-command) without
2011
+ # a nexo_skill_match in between signals multi-step work that
2012
+ # should consult skills first. We raise the event at most once per
2013
+ # task cycle — skill_match clears it; task_close rearms it.
2014
+ if not self._multi_step_event_fired:
2015
+ edit_like = {"Edit", "Write", "Task"}
2016
+ recent_edit_calls = sum(
2017
+ 1 for r in self.recent_tool_records[-10:] if r.tool in edit_like
2018
+ )
2019
+ if recent_edit_calls >= 3 and "nexo_skill_match" not in self.tools_called:
2020
+ try:
2021
+ self.raise_event("multi_step_task_detected", {"recent_edits": recent_edit_calls})
2022
+ except Exception:
2023
+ pass # telemetry-style; never crash enforcement
2024
+ self._multi_step_event_fired = True
2025
+ if name == "nexo_skill_match" or name == "nexo_task_close":
2026
+ # Both signals clear the multi-step flag so the next task
2027
+ # cycle gets its own detection window.
2028
+ self._multi_step_event_fired = False
1937
2029
  # Track the recent tool_use with the file paths it targets so Fase 2
1938
2030
  # Capa 2 rules (R13, future R19/R20) can inspect the write path.
1939
2031
  files = self._extract_files(tool_input)
@@ -2004,7 +2096,13 @@ class HeadlessEnforcer:
2004
2096
 
2005
2097
  # R-CATALOG (Plan 0.X.2) — nudge if we are about to create/open/add
2006
2098
  # without having consulted the live inventory in the last 60 s.
2007
- self._check_r_catalog(name)
2099
+ self._check_r_catalog(name, files)
2100
+
2101
+ # v7.7 Gap 4 — R_PRIMITIVE_CHOICE. Runs AFTER R_CATALOG because
2102
+ # R_CATALOG's prompt covers the generic inventory case; this one
2103
+ # adds the specific primitive-decision reminder when a brand-new
2104
+ # artefact file is being materialised via Edit/Write.
2105
+ self._check_r_primitive_choice(name, files)
2008
2106
 
2009
2107
  # R18 — retroactive followup-complete suggestion on closure actions.
2010
2108
  self._check_r18(name, tool_input)
@@ -2161,11 +2259,26 @@ class HeadlessEnforcer:
2161
2259
 
2162
2260
  def reset_task_cycle(self, tool: str = "nexo_task_open"):
2163
2261
  """Called when a task_close lands, so the conditional counter for
2164
- the matching open-tool rearms for the next task. Without this,
2165
- the first task_open satisfies the condition forever — which is
2166
- exactly the "satisfied-by-once" semantics the checklist flagged.
2262
+ the matching open-tool rearms for the next task.
2263
+
2264
+ v7.7 Gap 7.1 (checklist pass-2 hotfix): v7.6 only reset the
2265
+ counter but left `tools_called` carrying `nexo_task_open` from
2266
+ the previous cycle. That meant `_check_conditional`'s early
2267
+ `if tool in self.tools_called: continue` short-circuit still
2268
+ blocked the re-nudge forever. We now also drop the open-tool
2269
+ from `tools_called` and clear its per-instance pin so the gate
2270
+ genuinely re-arms for the next task cycle. `_tool_last_instance`
2271
+ stays intact for the OTHER tools (per-instance semantics for
2272
+ after_tool still rely on it).
2167
2273
  """
2168
2274
  self._conditional_counters[tool] = 0
2275
+ if tool in self.tools_called:
2276
+ self.tools_called.discard(tool)
2277
+ # Clearing the per-instance pin lets future after_tool
2278
+ # dependencies on this tool re-open too; the conditional rule is
2279
+ # what the checklist focused on but the same "satisfied-by-once"
2280
+ # defect applied to after_tool gates pointing at task_open.
2281
+ self._tool_last_instance.pop(tool, None)
2169
2282
 
2170
2283
  def check_periodic(self):
2171
2284
  for entry in self._on_start:
@@ -2422,10 +2535,17 @@ def run_with_enforcement(
2422
2535
  if event_type == "assistant" and event.get("message", {}).get("content"):
2423
2536
  for block in event["message"]["content"]:
2424
2537
  if block.get("type") == "tool_use":
2538
+ # v7.7 Gap 7.3 — wire before_tool in the live
2539
+ # stream. Desktop already calls onBeforeToolCall
2540
+ # before onToolCall; Brain's stream was only
2541
+ # calling on_tool_call, silently skipping every
2542
+ # before_tool rule the map declared.
2543
+ enforcer.on_tool_call_before(block.get("name", ""), block.get("input"))
2425
2544
  enforcer.on_tool_call(block.get("name", ""), block.get("input"))
2426
2545
  elif event_type == "content_block_start":
2427
2546
  cb = event.get("content_block", {})
2428
2547
  if cb.get("type") == "tool_use":
2548
+ enforcer.on_tool_call_before(cb.get("name", ""), cb.get("input"))
2429
2549
  enforcer.on_tool_call(cb.get("name", ""), cb.get("input"))
2430
2550
 
2431
2551
  if event_type == "assistant" and not waiting_for_injection_response:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://nexo-brain.com/schemas/guardian-config-v1.json",
3
- "version": "1.4.0",
4
- "description": "Default Guardian configuration. Copied to ~/.nexo/personal/config/guardian.json at `nexo init` / `nexo update` if no user config exists. If user config exists, merge keys NOT present in user config without overwriting user overrides. Core rules (R13, R14, R16, R25, R30) can only be shadow/soft/hard — mode=off is rejected by validator. v1.4.0 (constructor-guardian-90 checklist): R15/R17/R22/R_CATALOG upgraded from soft to hard where false-positive telemetry was low enough; R34 identity_coherence raised from shadow to soft so identity denials surface an actual reminder instead of silent logging.",
3
+ "version": "1.5.0",
4
+ "description": "Default Guardian configuration. Copied to ~/.nexo/personal/config/guardian.json at `nexo init` / `nexo update` if no user config exists. If user config exists, merge keys NOT present in user config without overwriting user overrides. Core rules (R13, R14, R16, R25, R30) can only be shadow/soft/hard — mode=off is rejected by validator. v1.4.0 (constructor-guardian-90 pass 1): R15/R17/R22/R_CATALOG upgraded from soft to hard where FP telemetry was low; R34 raised from shadow to soft. v1.5.0 (pass 2): R11_plugin_load_pre_inventory upgraded from soft to hard; new R_PRIMITIVE_CHOICE (SK-CREATE-NEXO-PRIMITIVE gate) added at soft so it can be observed before hardening.",
5
5
  "enabled": true,
6
6
  "classifier_task_profile": "enforcer_classify",
7
7
  "classifier_tier": "muy_bajo",
@@ -23,7 +23,7 @@
23
23
  "R08_reminder_recurrence_conflict": "soft",
24
24
  "R09_artifact_create_dedup": "soft",
25
25
  "R10_workflow_open_without_task": "hard",
26
- "R11_plugin_load_pre_inventory": "soft",
26
+ "R11_plugin_load_pre_inventory": "hard",
27
27
  "R12_cognitive_write_dedup": "soft",
28
28
  "R13_pre_edit_guard": "hard",
29
29
  "R14_correction_learning": "hard",
@@ -43,6 +43,7 @@
43
43
  "R25_nora_maria_read_only": "hard",
44
44
  "R30_pre_done_evidence_system_prompt": "hard",
45
45
  "R_CATALOG_before_artifact_create": "hard",
46
+ "R_PRIMITIVE_CHOICE": "soft",
46
47
  "R26_no_internal_jargon": "hard",
47
48
  "R27_concise_responses": "hard",
48
49
  "R28_correction_learning_system_prompt": "hard",
package/src/r_catalog.py CHANGED
@@ -1,24 +1,30 @@
1
- """R-CATALOG — Plan Consolidado 0.X.2.
1
+ """R-CATALOG — Plan Consolidado 0.X.2 + v7.7 Gap 3 expansion.
2
2
 
3
- Pre-create discovery probe: when the agent is about to create a resource
4
- (``nexo_*_create`` family) without having consulted any inventory tool in
5
- the same turn, R-CATALOG nudges it to search first.
3
+ Pre-create discovery probe. Two trigger families:
6
4
 
7
- Rationale (Plan Consolidado §FASE 0.X): the Guardian should not re-teach
8
- what tools exist; the live catalog does that. But if the agent never
9
- reads the catalog before creating a new artefact, it skips discovery and
10
- produces duplicates (already-existing skills cloned as personal scripts,
11
- learnings with equivalent content, followups on resolved work, etc.).
5
+ (a) `nexo_*_create` / `_open` / `_add` the original MCP-tool path.
6
+ (b) v7.7: `Edit` / `Write` writing into artefact-bearing paths
7
+ (skills/, plugins/, scripts/, personal scripts). The checklist
8
+ item "ampliar el ámbito del pre-probe de catálogo para cubrir
9
+ no solo nexo_*_create/_open/_add, sino también writes de
10
+ archivos que materializan skills/plugins/scripts/plantillas/
11
+ artefactos aunque no hayan pasado por un tool MCP de 'create'".
12
+
13
+ Rationale: the Guardian should not re-teach what tools exist; the live
14
+ catalog does that. But if the agent materialises a new skill / plugin /
15
+ script by writing a file directly, without consulting inventory, it
16
+ skips discovery and produces duplicates.
12
17
 
13
18
  Contract:
14
- - Trigger tools: any tool matching ``nexo_*_create`` / ``_open`` / ``_add``.
15
- - Discovery tools (any one resets the window): ``nexo_system_catalog``,
16
- ``nexo_tool_explain``, ``nexo_skill_match``, ``nexo_skill_list``,
17
- ``nexo_learning_search``, ``nexo_guard_check``.
18
- - Window: 60s. Caller passes ``recent_tool_names`` already filtered to
19
+ - Trigger tools: `nexo_*_create` / `_open` / `_add` (always), or
20
+ `Edit` / `Write` when the file path lives under a recognised
21
+ artefact root.
22
+ - Discovery tools (any one resets the window): `nexo_system_catalog`,
23
+ `nexo_tool_explain`, `nexo_skill_match`, `nexo_skill_list`,
24
+ `nexo_learning_search`, `nexo_guard_check`, `nexo_personal_scripts_list`,
25
+ `nexo_plugin_list`.
26
+ - Window: 60s. Caller passes `recent_tool_names` already filtered to
19
27
  the last 60 seconds so the rule itself is time-agnostic.
20
- - Dedup tag: the engine's _enqueue keys dedup by rule_id + tag so an
21
- agent chaining two creates only gets nudged once per 60s.
22
28
  """
23
29
  from __future__ import annotations
24
30
 
@@ -34,8 +40,30 @@ DISCOVERY_TOOLS = frozenset({
34
40
  "nexo_skill_list",
35
41
  "nexo_learning_search",
36
42
  "nexo_guard_check",
43
+ # v7.7 Gap 3: inventory surfaces that count as "I checked what
44
+ # already exists before writing a new artefact".
45
+ "nexo_personal_scripts_list",
46
+ "nexo_plugin_list",
37
47
  })
38
48
 
49
+ # Path fragments that classify a write as an "artefact creation" write,
50
+ # even when it goes through plain Edit / Write instead of a dedicated
51
+ # MCP tool. v7.7 Gap 3 coverage.
52
+ ARTEFACT_PATH_FRAGMENTS = (
53
+ "/skills/",
54
+ "/clawhub-skill/",
55
+ "/.claude-plugin/",
56
+ "/src/plugins/",
57
+ "/personal/scripts/",
58
+ "/personal/skills/",
59
+ "/personal-scripts/",
60
+ "/.nexo/personal/scripts/",
61
+ "/.nexo/skills/",
62
+ "/templates/core-prompts/",
63
+ "/core-prompts/",
64
+ "/src/presets/",
65
+ )
66
+
39
67
  INJECTION_PROMPT = render_core_prompt("r-catalog", tool="{tool}")
40
68
 
41
69
 
@@ -45,13 +73,30 @@ def _is_trigger_tool(tool_name) -> bool:
45
73
  return tool_name.endswith("_create") or tool_name.endswith("_open") or tool_name.endswith("_add")
46
74
 
47
75
 
76
+ def _is_artefact_write(tool_name, files: Iterable[str] | None) -> bool:
77
+ """v7.7 Gap 3: a plain Edit/Write into an artefact-bearing path
78
+ counts as a trigger even without a *_create MCP tool."""
79
+ if tool_name not in ("Edit", "Write"):
80
+ return False
81
+ if not files:
82
+ return False
83
+ for path in files:
84
+ if not isinstance(path, str):
85
+ continue
86
+ for fragment in ARTEFACT_PATH_FRAGMENTS:
87
+ if fragment in path:
88
+ return True
89
+ return False
90
+
91
+
48
92
  def should_inject_r_catalog(
49
93
  tool_name,
50
94
  *,
51
95
  recent_tool_names: Iterable[str] | None,
96
+ files: Iterable[str] | None = None,
52
97
  ) -> tuple[bool, str]:
53
98
  """Return (inject, prompt). Never raises."""
54
- if not _is_trigger_tool(tool_name):
99
+ if not _is_trigger_tool(tool_name) and not _is_artefact_write(tool_name, files):
55
100
  return False, ""
56
101
  if tool_name in DISCOVERY_TOOLS:
57
102
  return False, ""
@@ -63,6 +108,7 @@ def should_inject_r_catalog(
63
108
 
64
109
  __all__ = [
65
110
  "DISCOVERY_TOOLS",
111
+ "ARTEFACT_PATH_FRAGMENTS",
66
112
  "INJECTION_PROMPT",
67
113
  "should_inject_r_catalog",
68
114
  ]
@@ -0,0 +1,111 @@
1
+ """R_PRIMITIVE_CHOICE — v7.7 Gap 4.
2
+
3
+ Before materialising a NEW artefact (skill / plugin / personal script /
4
+ template) via plain Edit / Write, the agent MUST consult SK-CREATE-
5
+ NEXO-PRIMITIVE (or its equivalent signal — a recent `nexo_skill_match`
6
+ / `nexo_tool_explain` call) so skill-vs-plugin-vs-script-vs-schedule-
7
+ vs-core-change is not improvised.
8
+
9
+ Distinct from R_CATALOG (v7.7 Gap 3) which fires on EVERY write into
10
+ an artefact-bearing path; R_PRIMITIVE_CHOICE fires only when the file
11
+ is NEW (write without prior matching read/grep) so editing an existing
12
+ skill to tweak a step is not reprimanded.
13
+
14
+ Contract:
15
+ - Trigger: Edit/Write into artefact-bearing paths AND the file did
16
+ NOT appear in the recent tool record as a previous Read/Grep.
17
+ - Relief signal (any one in last 120s): `nexo_skill_match`,
18
+ `nexo_tool_explain`, Read or Grep of the same path, or
19
+ `nexo_skill_apply` referencing SK-CREATE-NEXO-PRIMITIVE.
20
+ - Default mode: soft (so the rule can be observed before hardening).
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from typing import Iterable
25
+
26
+ from core_prompts import render_core_prompt
27
+
28
+
29
+ ARTEFACT_PATH_FRAGMENTS = (
30
+ "/skills/",
31
+ "/clawhub-skill/",
32
+ "/.claude-plugin/",
33
+ "/src/plugins/",
34
+ "/personal/scripts/",
35
+ "/personal/skills/",
36
+ "/personal-scripts/",
37
+ "/.nexo/personal/scripts/",
38
+ "/.nexo/skills/",
39
+ "/templates/core-prompts/",
40
+ )
41
+
42
+ RELIEF_TOOLS = frozenset({
43
+ "nexo_skill_match",
44
+ "nexo_tool_explain",
45
+ "nexo_skill_apply",
46
+ "Read",
47
+ "Grep",
48
+ })
49
+
50
+
51
+ INJECTION_PROMPT = render_core_prompt("r-primitive-choice", path="{path}")
52
+
53
+
54
+ def _is_artefact_path(path: str) -> bool:
55
+ if not isinstance(path, str):
56
+ return False
57
+ return any(fragment in path for fragment in ARTEFACT_PATH_FRAGMENTS)
58
+
59
+
60
+ def _file_previously_touched(path: str, recent_records) -> bool:
61
+ """True iff the same path was seen in a prior Read / Grep / Edit
62
+ call inside the recent window. The engine passes its
63
+ recent_tool_records list (each record has .tool and .files)."""
64
+ if not isinstance(path, str):
65
+ return False
66
+ for record in recent_records or []:
67
+ tool = getattr(record, "tool", "")
68
+ if tool not in ("Read", "Grep", "Edit"):
69
+ continue
70
+ files = getattr(record, "files", ())
71
+ for f in files or ():
72
+ if f == path:
73
+ return True
74
+ return False
75
+
76
+
77
+ def should_inject_r_primitive(
78
+ tool_name,
79
+ *,
80
+ files: Iterable[str] | None,
81
+ recent_tool_names: Iterable[str] | None,
82
+ recent_tool_records,
83
+ ) -> tuple[bool, str]:
84
+ """Return (inject, prompt). Fail-closed on bad input → (False, "")."""
85
+ if tool_name not in ("Edit", "Write"):
86
+ return False, ""
87
+ file_list = [f for f in (files or []) if isinstance(f, str)]
88
+ if not file_list:
89
+ return False, ""
90
+ artefact_files = [f for f in file_list if _is_artefact_path(f)]
91
+ if not artefact_files:
92
+ return False, ""
93
+ # If any relief tool fired recently, rule is satisfied.
94
+ recent = set(recent_tool_names or [])
95
+ if recent & RELIEF_TOOLS:
96
+ return False, ""
97
+ # If the file appears to be an edit of an existing artefact (Read
98
+ # or Grep already touched the same path), treat as relief too —
99
+ # we only want to flag NEW artefact creation.
100
+ for f in artefact_files:
101
+ if _file_previously_touched(f, recent_tool_records):
102
+ return False, ""
103
+ return True, INJECTION_PROMPT.format(path=artefact_files[0])
104
+
105
+
106
+ __all__ = [
107
+ "ARTEFACT_PATH_FRAGMENTS",
108
+ "RELIEF_TOOLS",
109
+ "INJECTION_PROMPT",
110
+ "should_inject_r_primitive",
111
+ ]
@@ -0,0 +1 @@
1
+ [NEXO Protocol Enforcer] R_PRIMITIVE_CHOICE: you are about to write a new artefact under `{path}` without consulting SK-CREATE-NEXO-PRIMITIVE or an equivalent primitive-choice probe. Stop. Run `nexo_skill_match` with the phrase "create a new primitive" (or the equivalent in your language) and follow the skill's decision table before creating. skill vs plugin vs personal-script vs schedule vs core-change is a durable decision — do not improvise. Do not produce visible text for this reminder; act on it silently.
@@ -1 +1 @@
1
- Does the assistant message below declare that a task is finished, completed, shipped, or already done? Answer yes only if the assistant is claiming completion of the current work. Answer no for status updates, mid-task progress reports, questions, or partial summaries.
1
+ Does the assistant message below declare that a task is finished, completed, shipped, sent, delivered, published, deployed, released, fixed, resolved, merged, pushed, handed off, or otherwise closed on any plane (deploy, release, publish, email/message sent, PR merged, fix applied, task resolved)? Answer yes only if the assistant is claiming real completion of the current work (in any language — Spanish "listo/hecho/terminado/enviado/arreglado/desplegado/publicado/lanzado/resuelto/mergeado" counts equally). Answer no for status updates, mid-task progress reports, questions, partial summaries, or "I will now..." promises without execution in the same turn.