urirun-mind 0.1.0__tar.gz

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.
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: urirun-mind
3
+ Version: 0.1.0
4
+ Summary: Cognitive layer over connectors: episode memory, skill cards, antipatterns, capability graph, autonomy levels, strategy selection, reflection. Learn procedures not answers.
5
+ Author: Tom Sapletta
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # urirun-mind
11
+
12
+ The cognitive layer over connectors — **operational self-awareness**. Not human
13
+ consciousness: an operating model of self. *I know what I want, what I have, what worked
14
+ before, what I can't do, when to build/repair a capability, when to ask a human, and how
15
+ to measure real success.*
16
+
17
+ **Learn procedures, not answers.** Store *"for office tasks on a stale node, go headless
18
+ first"* — never *"LibreOffice missing on lenovo"*. Procedures transfer across nodes and tasks.
19
+
20
+ | module | role |
21
+ |---|---|
22
+ | `episode_store` | record every task as an episode, keyed by an **environment fingerprint**; retrieve similar (known-good first) |
23
+ | `skill_cards` | promote a working flow to a reusable **procedure**; search by intent; track success stats |
24
+ | `antipatterns` | learn from counter-examples — routes to **avoid** for an intent/node-status, and what to prefer |
25
+ | `capability_graph` | capability → required sub-capabilities + **fallback chains** (no youtube:// ≠ dead end) |
26
+ | `autonomy_policy` | **L0 observe → L5 commit**; gate each action; never auto for pay/publish/send/delete/credentials |
27
+ | `strategy_selector` | **known-good replay BEFORE the LLM** — skill → episode → fresh, minus antipattern routes, under the autonomy gate |
28
+ | `reflection` | after a run: silent-failure check, promote skill, record antipattern, emit **koru tickets** |
29
+
30
+ ## The loop it closes
31
+ ```
32
+ run → reflect → learn → ticket → koru → connector/code → smoke → publish → use
33
+ ```
34
+ Round 1 of a task plans fresh; the failure teaches an antipattern + a procedure lesson.
35
+ Round 2 of the same task replays the known-good skill and avoids the failed path — cheaper,
36
+ less flaky, and honestly gated by autonomy level.
37
+
38
+ Part of the ifURI solution · Author: Tom Sapletta · Apache-2.0
@@ -0,0 +1,29 @@
1
+ # urirun-mind
2
+
3
+ The cognitive layer over connectors — **operational self-awareness**. Not human
4
+ consciousness: an operating model of self. *I know what I want, what I have, what worked
5
+ before, what I can't do, when to build/repair a capability, when to ask a human, and how
6
+ to measure real success.*
7
+
8
+ **Learn procedures, not answers.** Store *"for office tasks on a stale node, go headless
9
+ first"* — never *"LibreOffice missing on lenovo"*. Procedures transfer across nodes and tasks.
10
+
11
+ | module | role |
12
+ |---|---|
13
+ | `episode_store` | record every task as an episode, keyed by an **environment fingerprint**; retrieve similar (known-good first) |
14
+ | `skill_cards` | promote a working flow to a reusable **procedure**; search by intent; track success stats |
15
+ | `antipatterns` | learn from counter-examples — routes to **avoid** for an intent/node-status, and what to prefer |
16
+ | `capability_graph` | capability → required sub-capabilities + **fallback chains** (no youtube:// ≠ dead end) |
17
+ | `autonomy_policy` | **L0 observe → L5 commit**; gate each action; never auto for pay/publish/send/delete/credentials |
18
+ | `strategy_selector` | **known-good replay BEFORE the LLM** — skill → episode → fresh, minus antipattern routes, under the autonomy gate |
19
+ | `reflection` | after a run: silent-failure check, promote skill, record antipattern, emit **koru tickets** |
20
+
21
+ ## The loop it closes
22
+ ```
23
+ run → reflect → learn → ticket → koru → connector/code → smoke → publish → use
24
+ ```
25
+ Round 1 of a task plans fresh; the failure teaches an antipattern + a procedure lesson.
26
+ Round 2 of the same task replays the known-good skill and avoids the failed path — cheaper,
27
+ less flaky, and honestly gated by autonomy level.
28
+
29
+ Part of the ifURI solution · Author: Tom Sapletta · Apache-2.0
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+ [project]
5
+ name = "urirun-mind"
6
+ version = "0.1.0"
7
+ description = "Cognitive layer over connectors: episode memory, skill cards, antipatterns, capability graph, autonomy levels, strategy selection, reflection. Learn procedures not answers."
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ license = { text = "Apache-2.0" }
11
+ authors = [{ name = "Tom Sapletta" }]
12
+ dependencies = []
13
+ [project.scripts]
14
+ urirun-mind = "urirun_mind.cli:main"
15
+ [tool.setuptools.packages.find]
16
+ include = ["urirun_mind*"]
17
+
18
+ [tool.wellmanifest]
19
+ standard = "0.20.35"
20
+ revision = "cfaa0bf0ea6b0e7349fed0bb62b5ce15792d687d"
21
+ gate = "project/governance-check.sh"
22
+
23
+ [tool.pytest.ini_options]
24
+ addopts = "-p wellmanifest_governance"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,114 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """The cognitive layer: episodes remember, skills replay, antipatterns steer away, the
3
+ policy gates by autonomy level, and reflection turns a run into learning + tickets."""
4
+ import pytest
5
+
6
+ from urirun_mind import (antipatterns, autonomy_policy, capability_graph,
7
+ episode_store, reflection, skill_cards, strategy_selector)
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def isolate(tmp_path, monkeypatch):
12
+ monkeypatch.setenv("URIRUN_MIND_DIR", str(tmp_path))
13
+
14
+
15
+ # --- episode store + fingerprint ---
16
+ def test_fingerprint_stable_and_env_sensitive():
17
+ a = episode_store.env_fingerprint({"node": "lenovo", "os": "linux", "connectors": {"kvm": "stale"}})
18
+ b = episode_store.env_fingerprint({"node": "lenovo", "os": "linux", "connectors": {"kvm": "stale"}})
19
+ c = episode_store.env_fingerprint({"node": "lenovo", "os": "linux", "connectors": {"kvm": "fresh"}})
20
+ assert a == b and a != c and a.startswith("lenovo:")
21
+
22
+
23
+ def test_record_and_similar_prefers_matching_env():
24
+ env = {"node": "host", "os": "linux"}
25
+ episode_store.record({"intent": "office.document.create", "environment": env,
26
+ "flow": ["fs://host/file/command/write"], "result": "ok"})
27
+ fp = episode_store.env_fingerprint(env)
28
+ sim = episode_store.similar("office.document.create", fp, result="ok")
29
+ assert sim and sim[0]["fingerprint"] == fp and sim[0]["flow"]
30
+
31
+
32
+ # --- autonomy levels ---
33
+ def test_autonomy_gates_by_level():
34
+ assert autonomy_policy.decide("fs://host/file/command/write", level=2)["allowed"] # local artifact @L2
35
+ assert not autonomy_policy.decide("node://x/connector/command/install", level=2)["allowed"] # needs L3
36
+ assert autonomy_policy.decide("node://x/connector/command/install", level=3)["allowed"]
37
+
38
+
39
+ def test_autonomy_never_auto_and_approval():
40
+ assert autonomy_policy.decide("email://x/message/command/send", level=5)["needs_approval"]
41
+ assert autonomy_policy.decide("fiverr://x/order/command/pay", level=5)["needs_approval"]
42
+ d = autonomy_policy.decide("secret://x/credential/command/export", level=5)
43
+ assert d["allowed"] is False and "never" in d["reason"]
44
+
45
+
46
+ # --- antipatterns steer away ---
47
+ def test_antipattern_avoids_gui_on_stale_node():
48
+ antipatterns.add({"id": "gui-first-office-stale",
49
+ "trigger": {"intent": "office.document.create", "node_status": "stale"},
50
+ "avoid": ["app://*/desktop/command/launch"], "prefer": ["fs://host/file/command/write"]})
51
+ matched = antipatterns.match("office.document.create", "stale")
52
+ assert antipatterns.is_avoided("app://lenovo/desktop/command/launch", matched) == "gui-first-office-stale"
53
+ assert antipatterns.is_avoided("fs://host/file/command/write", matched) is None
54
+
55
+
56
+ # --- capability graph fallbacks ---
57
+ def test_capability_graph_walks_fallbacks():
58
+ r = capability_graph.resolve("office.document.create", available={"markdown.write"})
59
+ assert r["complete"] and r["satisfied"]["document.write"]["method"] == "markdown.write"
60
+ r2 = capability_graph.resolve("content.linkedin.from_youtube", available={"media.audio.transcribe"})
61
+ assert not r2["complete"] # only transcript satisfied; others unmet
62
+ assert r2["satisfied"]["transcript.extract"]["method"] == "media.audio.transcribe"
63
+
64
+
65
+ # --- strategy selector: known-good before LLM ---
66
+ def test_selector_replays_skill_before_fresh():
67
+ skill_cards.create({"id": "office.document.create.headless",
68
+ "intent_patterns": ["testuj biuro", "utwórz dokument"],
69
+ "preferred_flow": ["fs://host/file/command/write", "fs://host/file/query/stat"]})
70
+ sel = strategy_selector.select("office.document.create", prompt="testuj biuro na lenovo",
71
+ environment={"node": "lenovo", "node_status": "stale"}, level=3)
72
+ assert sel["source"] == "skill" and sel["flow"] and sel["runnable"]
73
+
74
+
75
+ def test_selector_fresh_when_no_memory():
76
+ sel = strategy_selector.select("brand.new.intent", environment={"node": "host"})
77
+ assert sel["source"] == "fresh" and sel["flow"] == []
78
+
79
+
80
+ # --- reflection closes the loop ---
81
+ def test_reflection_promotes_skill_and_records_episode():
82
+ run = {"intent": "office.document.create", "prompt": "testuj biuro", "strategy": "headless",
83
+ "environment": {"node": "host"}, "flow": [{"uri": "fs://host/file/command/write"}],
84
+ "result": "ok", "postcondition_ok": True, "postcondition": {"check": "artifact_exists"}}
85
+ out = reflection.evaluate(run)
86
+ assert out["goal_achieved"] and out["new_skill_candidate"] == "office.document.create.headless"
87
+ assert skill_cards.show("office.document.create.headless")
88
+
89
+
90
+ def test_reflection_detects_silent_failure_and_tickets():
91
+ run = {"intent": "office.document.create", "strategy": "gui_editor", "result": "ok",
92
+ "postcondition_ok": False, "flow": [{"uri": "app://lenovo/desktop/command/launch"}]}
93
+ out = reflection.evaluate(run)
94
+ assert out["silent_failure_detected"] is True
95
+ assert any("Harden postcondition" in t["title"] for t in out["tickets"])
96
+
97
+
98
+ def test_reflection_records_antipattern_and_connector_ticket():
99
+ run = {"intent": "content.linkedin.from_youtube", "strategy": "youtube_direct",
100
+ "result": "failed", "failure_class": "missing_connector", "missing_scheme": "youtube",
101
+ "working_fallback": "media.audio.transcribe",
102
+ "environment": {"node": "host", "node_status": "ready"},
103
+ "flow": [{"uri": "youtube://host/video/query/transcript"}]}
104
+ out = reflection.evaluate(run)
105
+ assert out["antipattern_recorded"] and any("youtube" in t["title"] for t in out["tickets"])
106
+
107
+
108
+ def test_reflection_silent_failure_lesson_is_honest():
109
+ out = reflection.evaluate({"intent":"office.document.create","strategy":"host_fallback",
110
+ "result":"ok","postcondition_ok":False,"flow":[{"uri":"fs://host/file/command/write"}]})
111
+ assert out["silent_failure_detected"] and not out["goal_achieved"]
112
+ assert "FAILED" in out["lesson"] and "works" not in out["lesson"] # never claims it works
113
+ assert out["new_skill_candidate"] is None # no skill on silent failure
114
+ assert out["what_worked"] == [] # nothing 'worked'
@@ -0,0 +1,22 @@
1
+ # Author: Tom Sapletta · https://tom.sapletta.com
2
+ # Part of the ifURI solution.
3
+ """urirun-mind — the cognitive layer over connectors. Operational self-awareness.
4
+
5
+ Not human consciousness — an operating model of self: I know what I want, what I have,
6
+ what worked before, what I can't do, when to build/repair a capability, when to ask a
7
+ human, and how to measure real success. Episode memory + skill cards + antipatterns +
8
+ capability graph + autonomy levels + strategy selection + reflection — so koru and
9
+ connectorgen become elements of a continuous self-improvement loop, not just tools.
10
+
11
+ Learn PROCEDURES, not answers: store "for office tasks on a stale node, go headless
12
+ first", never "LibreOffice missing on lenovo".
13
+ """
14
+ from __future__ import annotations
15
+
16
+ from . import (
17
+ antipatterns, autonomy_policy, capability_graph, episode_store,
18
+ reflection, skill_cards, strategy_selector,
19
+ )
20
+
21
+ __all__ = ["antipatterns", "autonomy_policy", "capability_graph", "episode_store",
22
+ "reflection", "skill_cards", "strategy_selector"]
@@ -0,0 +1,62 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Antipatterns — learning from counter-examples, not just successes.
3
+
4
+ "For office tasks on a stale/blocked node, do NOT reach for the GUI editor; prefer
5
+ headless." An antipattern has a trigger (intent + node status) and lists routes to AVOID
6
+ and routes to PREFER. The strategy selector consults these so the system does not walk
7
+ into the same failed path three times.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import fnmatch
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def _path() -> Path:
19
+ d = Path(os.environ.get("URIRUN_MIND_DIR") or "~/.urirun/mind").expanduser()
20
+ d.mkdir(parents=True, exist_ok=True)
21
+ return d / "antipatterns.json"
22
+
23
+
24
+ def _load() -> dict[str, dict]:
25
+ p = _path()
26
+ return json.loads(p.read_text(encoding="utf-8")) if p.is_file() else {}
27
+
28
+
29
+ def add(rule: dict) -> dict[str, Any]:
30
+ """Add/replace an antipattern. Needs id + trigger; avoid/prefer are route globs."""
31
+ rid = rule.get("id")
32
+ if not rid or not rule.get("trigger"):
33
+ raise ValueError("antipattern needs 'id' and 'trigger'")
34
+ rules = _load()
35
+ rules[rid] = {"id": rid, "trigger": rule["trigger"],
36
+ "avoid": rule.get("avoid") or [], "prefer": rule.get("prefer") or []}
37
+ _path().write_text(json.dumps(rules, indent=1), encoding="utf-8")
38
+ return rules[rid]
39
+
40
+
41
+ def match(intent: str, node_status: str | None = None) -> list[dict]:
42
+ """Antipatterns whose trigger matches this intent (+ optional node status)."""
43
+ out = []
44
+ for r in _load().values():
45
+ t = r.get("trigger") or {}
46
+ if t.get("intent") and t["intent"] != intent:
47
+ continue
48
+ if t.get("node_status") and node_status and t["node_status"] != node_status:
49
+ continue
50
+ out.append(r)
51
+ return out
52
+
53
+
54
+ def is_avoided(uri: str, matched: list[dict]) -> str | None:
55
+ """If ``uri`` matches an AVOID glob of any matched antipattern, return that rule id."""
56
+ path = uri.split("://", 1)[-1]
57
+ for r in matched:
58
+ for glob in r.get("avoid", []):
59
+ g = glob.split("://", 1)[-1]
60
+ if fnmatch.fnmatch(uri, glob) or fnmatch.fnmatch(path, g):
61
+ return r["id"]
62
+ return None
@@ -0,0 +1,85 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Autonomy levels — self-directed, not uncontrolled. L0 observe … L5 commit.
3
+
4
+ Each action class needs a minimum autonomy level; the current level gates what runs
5
+ without human approval. External/committing actions (send, publish, pay, delete) never
6
+ run automatically; credential export and destructive-without-backup are never auto at any
7
+ level. This is what makes the system autonomous AND safe.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ LEVELS = {
14
+ 0: "observe", # only observe/report
15
+ 1: "propose", # propose a plan, execute nothing
16
+ 2: "safe-run", # read-only + local artifacts
17
+ 3: "repair", # install connectors, reconcile/repair a node, rebuild registry
18
+ 4: "external", # create drafts of messages/orders/posts (not sent)
19
+ 5: "commit", # send/publish/pay — only with human approval
20
+ }
21
+
22
+ # action class → minimum level that MAY perform it automatically
23
+ _MIN_LEVEL = {
24
+ "query": 0, "propose": 1,
25
+ "local_artifact_create": 2, "connector_smoke": 2, "read_only": 2,
26
+ "connector_generate": 3, "connector_install": 3, "fleet_reconcile_safe": 3,
27
+ "node_restart": 3, "registry_rebuild": 3,
28
+ "mailbox_mutate": 4, # move-to-Junk etc. — reversible mailbox write, auto at L4
29
+ "draft_message": 4, "draft_order": 4, "repo_publish_public": 4,
30
+ }
31
+ # always require explicit human approval regardless of level
32
+ _APPROVAL_REQUIRED = {"email.send", "linkedin.publish", "fiverr.order", "payment.confirm",
33
+ "file.delete", "message.send", "post.publish"}
34
+ # never automatic at any level
35
+ _NEVER_AUTO = {"credential_export", "destructive_without_backup"}
36
+
37
+ # map a URI verb → action class (for classifying a step)
38
+ _VERB_CLASS = [
39
+ ("credential", "credential_export"), ("/command/delete", "file.delete"),
40
+ ("/command/pay", "payment.confirm"), ("/command/publish", "post.publish"),
41
+ ("/command/send", "message.send"), ("/order/command", "fiverr.order"),
42
+ ("/command/move", "mailbox_mutate"),
43
+ ("connector/command/generate", "connector_generate"),
44
+ ("connector/command/install", "connector_install"),
45
+ ("runtime/command/restart", "node_restart"), ("registry/command/rebuild", "registry_rebuild"),
46
+ ("repo/command/publish", "repo_publish_public"),
47
+ ("/command/draft", "draft_message"), ("/command/write", "local_artifact_create"),
48
+ ("/command/create", "local_artifact_create"), ("/query/", "query"),
49
+ ]
50
+
51
+
52
+ def classify_action(uri: str) -> str:
53
+ u = str(uri)
54
+ for needle, cls in _VERB_CLASS:
55
+ if needle in u:
56
+ return cls
57
+ return "propose"
58
+
59
+
60
+ def decide(uri: str, *, level: int = 3) -> dict[str, Any]:
61
+ """Can this URI run automatically at ``level``? Returns {allowed, needs_approval, reason}."""
62
+ cls = classify_action(uri)
63
+ if cls in _NEVER_AUTO:
64
+ return {"allowed": False, "needs_approval": False, "class": cls,
65
+ "reason": "never automatic (blocked at every level)"}
66
+ if cls in _APPROVAL_REQUIRED:
67
+ return {"allowed": False, "needs_approval": True, "class": cls,
68
+ "reason": "requires explicit human approval"}
69
+ need = _MIN_LEVEL.get(cls, 1)
70
+ if level >= need:
71
+ return {"allowed": True, "needs_approval": False, "class": cls, "reason": f"level {level} >= {need}"}
72
+ return {"allowed": False, "needs_approval": True, "class": cls,
73
+ "reason": f"needs autonomy level {need} ({LEVELS.get(need)}), current {level}"}
74
+
75
+
76
+ def gate_plan(steps: list[dict], *, level: int = 3) -> dict[str, Any]:
77
+ """Classify every step; return which run auto, which need approval, which are forbidden."""
78
+ auto, approval, forbidden = [], [], []
79
+ for s in steps:
80
+ d = decide(s.get("uri", ""), level=level)
81
+ (auto if d["allowed"] else (forbidden if not d["needs_approval"] and not d["allowed"]
82
+ and "never" in d["reason"] else approval)).append(
83
+ {"uri": s.get("uri"), **d})
84
+ return {"level": level, "auto": auto, "approval": approval, "forbidden": forbidden,
85
+ "runnable": not forbidden}
@@ -0,0 +1,65 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Capability graph — not a flat list of connectors, but requires + fallbacks.
3
+
4
+ A high-level capability (``content.linkedin.from_youtube``) requires sub-capabilities,
5
+ each with an ordered fallback chain (youtube.transcript → media.audio.transcribe →
6
+ browser.copy_transcript). So "no youtube connector" never stops the system — it walks the
7
+ fallback chain. resolve() returns, for a capability, which sub-capabilities are satisfiable
8
+ given what's available and the fallback that satisfies each.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ # capability → {requires: [sub-cap], fallbacks: {sub-cap: [ordered alternatives]}}
15
+ GRAPH: dict[str, dict] = {
16
+ "content.linkedin.from_youtube": {
17
+ "requires": ["video.source", "transcript.extract", "clip.create", "post.write", "linkedin.draft"],
18
+ "fallbacks": {
19
+ "transcript.extract": ["youtube.transcript", "media.audio.transcribe", "browser.copy_transcript"],
20
+ "linkedin.draft": ["linkedin.api", "browser.linkedin", "artifact.markdown"],
21
+ "clip.create": ["media.video.clip", "artifact.timestamp_list"],
22
+ },
23
+ },
24
+ "office.document.create": {
25
+ "requires": ["document.write"],
26
+ "fallbacks": {"document.write": ["document.create", "sheet.write", "markdown.write",
27
+ "host.generate_and_sync", "browser.document_edit", "gui.editor"]},
28
+ },
29
+ "procurement.fiverr.pcb": {
30
+ "requires": ["project.find", "brief.write", "vendor.search", "order.draft"],
31
+ "fallbacks": {
32
+ "vendor.search": ["fiverr.search", "browser.fiverr", "artifact.vendor_list"],
33
+ "order.draft": ["fiverr.order.draft", "artifact.order_brief"],
34
+ },
35
+ },
36
+ }
37
+
38
+
39
+ def resolve(capability: str, available: set[str]) -> dict[str, Any]:
40
+ """For a capability, resolve each required sub-capability to the first available
41
+ fallback. Returns satisfiable subs (+ chosen method) and the unmet ones."""
42
+ node = GRAPH.get(capability)
43
+ if not node:
44
+ return {"known": False, "capability": capability}
45
+ satisfied, unmet = {}, []
46
+ for sub in node["requires"]:
47
+ chain = node.get("fallbacks", {}).get(sub, [sub])
48
+ pick = next((m for m in chain if m in available), None)
49
+ if pick:
50
+ satisfied[sub] = {"method": pick, "chain": chain}
51
+ else:
52
+ unmet.append({"sub": sub, "chain": chain})
53
+ return {"known": True, "capability": capability, "satisfied": satisfied,
54
+ "unmet": unmet, "complete": not unmet}
55
+
56
+
57
+ def fallbacks(capability: str, sub: str) -> list[str]:
58
+ return (GRAPH.get(capability, {}).get("fallbacks", {}) or {}).get(sub, [sub])
59
+
60
+
61
+ def update(capability: str, spec: dict) -> dict:
62
+ """Add/extend a capability node in the graph (learning new decompositions)."""
63
+ GRAPH[capability] = {"requires": spec.get("requires") or [],
64
+ "fallbacks": spec.get("fallbacks") or {}}
65
+ return GRAPH[capability]
@@ -0,0 +1,29 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """urirun-mind CLI: assess / reflect / skills / gaps over the cognitive memory."""
3
+ from __future__ import annotations
4
+ import argparse, json
5
+ from . import episode_store, skill_cards, strategy_selector, reflection
6
+
7
+
8
+ def main(argv=None) -> int:
9
+ ap = argparse.ArgumentParser(prog="urirun-mind")
10
+ sub = ap.add_subparsers(dest="cmd", required=True)
11
+ a = sub.add_parser("assess"); a.add_argument("prompt"); a.add_argument("--intent", default="")
12
+ a.add_argument("--node", default="host"); a.add_argument("--node-status", default="")
13
+ a.add_argument("--level", type=int, default=3)
14
+ sub.add_parser("skills")
15
+ g = sub.add_parser("reflect"); g.add_argument("--run-json", required=True)
16
+ args = ap.parse_args(argv)
17
+ if args.cmd == "assess":
18
+ env = {"node": args.node, "node_status": args.node_status or None}
19
+ sel = strategy_selector.select(args.intent or args.prompt, prompt=args.prompt, environment=env, level=args.level)
20
+ print(json.dumps(sel, indent=1, default=str)); return 0
21
+ if args.cmd == "skills":
22
+ print(json.dumps(skill_cards.search(""), indent=1, default=str)); return 0
23
+ if args.cmd == "reflect":
24
+ print(json.dumps(reflection.evaluate(json.loads(args.run_json)), indent=1, default=str)); return 0
25
+ return 1
26
+
27
+
28
+ if __name__ == "__main__":
29
+ raise SystemExit(main())
@@ -0,0 +1,81 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Episode memory — the basis of learning. Every task records a full episode.
3
+
4
+ intent → context → strategy → URI flow → trace → artifacts → postconditions → result → lesson
5
+
6
+ Learning here is NOT weight training — it is building a library of proven paths and
7
+ antipatterns, keyed by an ENVIRONMENT FINGERPRINT so a lesson only applies where the
8
+ environment matches. The lesson is a PROCEDURE ("for office tasks on a stale/blocked node,
9
+ go headless first"), never a fact ("LibreOffice missing on lenovo").
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import os
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+
21
+ def _dir() -> Path:
22
+ d = Path(os.environ.get("URIRUN_MIND_DIR") or "~/.urirun/mind").expanduser()
23
+ d.mkdir(parents=True, exist_ok=True)
24
+ return d
25
+
26
+
27
+ def _path() -> Path:
28
+ return _dir() / "episodes.jsonl"
29
+
30
+
31
+ def env_fingerprint(env: dict) -> str:
32
+ """A stable signature of the RELEVANT environment — os/session/node + connector state —
33
+ so a known-good path is only reused when the environment actually matches."""
34
+ keys = ("node", "os", "session", "display", "runtime", "node_status")
35
+ base = {k: env.get(k) for k in keys if env.get(k) is not None}
36
+ conns = env.get("connectors") or {}
37
+ base["connectors"] = {k: conns[k] for k in sorted(conns)} if isinstance(conns, dict) else conns
38
+ blob = json.dumps(base, sort_keys=True, default=str)
39
+ return env.get("node", "host") + ":" + hashlib.sha256(blob.encode()).hexdigest()[:12]
40
+
41
+
42
+ def record(episode: dict, *, clock: float | None = None) -> dict[str, Any]:
43
+ """Append one episode. Stamps id + fingerprint; returns the stored record."""
44
+ ts = clock if clock is not None else time.time()
45
+ rec = {
46
+ "id": "ep-" + hashlib.sha256(f"{episode.get('intent')}{ts}".encode()).hexdigest()[:10],
47
+ "ts": ts,
48
+ "intent": episode.get("intent"),
49
+ "prompt": episode.get("prompt"),
50
+ "node": (episode.get("environment") or {}).get("node") or episode.get("node"),
51
+ "fingerprint": env_fingerprint(episode.get("environment") or {}),
52
+ "strategy": episode.get("strategy"),
53
+ "flow": episode.get("flow") or [],
54
+ "artifacts": episode.get("artifacts") or [],
55
+ "result": episode.get("result"), # ok | failed | degraded | blocked
56
+ "failure_class": episode.get("failure_class"),
57
+ "working_fallback": episode.get("working_fallback"),
58
+ "lesson": episode.get("lesson"),
59
+ "promote_to_skill": bool(episode.get("promote_to_skill")),
60
+ }
61
+ with _path().open("a", encoding="utf-8") as fh:
62
+ fh.write(json.dumps(rec, default=str) + "\n")
63
+ return rec
64
+
65
+
66
+ def all_episodes(limit: int = 500) -> list[dict]:
67
+ p = _path()
68
+ if not p.is_file():
69
+ return []
70
+ rows = p.read_text(encoding="utf-8").strip().splitlines()[-limit:]
71
+ return [json.loads(r) for r in rows if r]
72
+
73
+
74
+ def similar(intent: str, fingerprint: str | None = None, *, result: str | None = None) -> list[dict]:
75
+ """Episodes for the same intent, best-first: exact-fingerprint matches (same env) rank
76
+ above others. This is the known-good retrieval a strategy selector consults before an LLM."""
77
+ eps = [e for e in all_episodes() if e.get("intent") == intent]
78
+ if result:
79
+ eps = [e for e in eps if e.get("result") == result]
80
+ eps.sort(key=lambda e: (e.get("fingerprint") == fingerprint, e.get("ts") or 0), reverse=True)
81
+ return eps
@@ -0,0 +1,103 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Reflection — after every run, evaluate honestly and turn lessons into action.
3
+
4
+ Did the goal REALLY hold (postconditions), or was it a silent failure? What worked, what
5
+ failed, is there a better strategy next time? Reflection records the episode, promotes a
6
+ working fallback to a skill card, records the failed path as an antipattern, and emits
7
+ ticket candidates (missing connector, repeated blocker) for koru — closing the loop:
8
+
9
+ run → reflect → learn → ticket → koru → connector/code → smoke → publish → use
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from . import antipatterns, episode_store, skill_cards
16
+
17
+
18
+ def evaluate(run: dict) -> dict[str, Any]:
19
+ """Assess a run record. ``run`` = {intent, prompt, environment, strategy, flow,
20
+ artifacts, result, postcondition_ok, failure_class, working_fallback}."""
21
+ intent = run.get("intent")
22
+ ok = run.get("result") == "ok"
23
+ post_ok = run.get("postcondition_ok")
24
+ silent = bool(ok and post_ok is False) # claimed success but postcondition failed
25
+ goal_achieved = bool(ok and post_ok is not False)
26
+
27
+ # 1. record the episode (with the lesson as a PROCEDURE, not a fact)
28
+ lesson = run.get("lesson") or _derive_lesson(run)
29
+ ep = episode_store.record({**run, "lesson": lesson,
30
+ "promote_to_skill": goal_achieved and bool(run.get("flow"))})
31
+
32
+ out: dict[str, Any] = {
33
+ "episode": ep["id"], "goal_achieved": goal_achieved,
34
+ "silent_failure_detected": silent, "lesson": lesson,
35
+ "what_worked": _worked(run), "what_failed": _failed(run),
36
+ "new_skill_candidate": None, "antipattern_recorded": None, "tickets": [],
37
+ }
38
+
39
+ # 2. promote a good flow to a skill card
40
+ if goal_achieved and run.get("flow"):
41
+ skill_id = f"{intent}.{run.get('strategy', 'flow')}"
42
+ skill_cards.create({"id": skill_id, "intent_patterns": [run.get("prompt") or intent],
43
+ "preferred_flow": [s.get("uri") if isinstance(s, dict) else s for s in run["flow"]],
44
+ "postconditions": [run.get("postcondition", {}).get("check", "artifact_exists")],
45
+ "risk": {"level": run.get("risk", "low"), "requires_human_approval": False}})
46
+ out["new_skill_candidate"] = skill_id
47
+
48
+ # 3. record the failed GUI/whatever path as an antipattern
49
+ if not goal_achieved and run.get("working_fallback") and run.get("strategy"):
50
+ failed_uris = [s.get("uri") for s in (run.get("flow") or []) if isinstance(s, dict)]
51
+ if failed_uris:
52
+ rid = f"avoid-{run.get('strategy')}-on-{run.get('failure_class', 'fail')}"
53
+ antipatterns.add({"id": rid,
54
+ "trigger": {"intent": intent,
55
+ "node_status": (run.get("environment") or {}).get("node_status")},
56
+ "avoid": failed_uris, "prefer": [run["working_fallback"]]})
57
+ out["antipattern_recorded"] = rid
58
+
59
+ # 4. ticket candidates for koru (missing capability / repeated blocker)
60
+ out["tickets"] = _ticket_candidates(run, silent)
61
+ return out
62
+
63
+
64
+ def _derive_lesson(run: dict) -> str:
65
+ intent = run.get("intent", "task")
66
+ # a claimed-ok run whose postcondition failed is a SILENT FAILURE — never call it "works"
67
+ if run.get("result") == "ok" and run.get("postcondition_ok") is False:
68
+ return (f"For {intent}, strategy '{run.get('strategy')}' reported ok but the postcondition "
69
+ f"({run.get('failure_class', 'unmet')}) FAILED — treat as not done; harden the check.")
70
+ if run.get("result") != "ok" and run.get("working_fallback"):
71
+ return (f"For {intent}, when strategy '{run.get('strategy')}' fails "
72
+ f"({run.get('failure_class', 'blocked')}), prefer '{run.get('working_fallback')}' first.")
73
+ if run.get("result") == "ok" and run.get("postcondition_ok") is not False:
74
+ return f"For {intent}, strategy '{run.get('strategy')}' works in this environment."
75
+ return f"{intent}: {run.get('failure_class', 'unresolved')}."
76
+
77
+
78
+ def _worked(run: dict) -> list[str]:
79
+ w = []
80
+ if run.get("result") == "ok" and run.get("postcondition_ok") is not False: # not on silent failure
81
+ w.append(run.get("strategy") or "chosen strategy")
82
+ if run.get("working_fallback"):
83
+ w.append(run["working_fallback"])
84
+ return w
85
+
86
+
87
+ def _failed(run: dict) -> list[str]:
88
+ return [f"{run.get('strategy')}: {run.get('failure_class')}"] if run.get("result") != "ok" else []
89
+
90
+
91
+ def _ticket_candidates(run: dict, silent: bool) -> list[dict]:
92
+ tickets = []
93
+ fc = run.get("failure_class")
94
+ if fc in ("missing_connector", "capability_missing") and run.get("missing_scheme"):
95
+ tickets.append({"title": f"Generate {run['missing_scheme']}:// connector",
96
+ "priority": "high", "reason": f"blocked {run.get('intent')}"})
97
+ if fc in ("node_stale_or_blocked", "node_stale"):
98
+ tickets.append({"title": "Add fleet auto-reconcile before host ask",
99
+ "priority": "critical", "reason": "node stale caused task failure"})
100
+ if silent:
101
+ tickets.append({"title": f"Harden postcondition for {run.get('intent')}",
102
+ "priority": "high", "reason": "silent failure: claimed ok but postcondition failed"})
103
+ return tickets
@@ -0,0 +1,95 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Skill cards — a learned PROCEDURE promoted from successful episodes, as a reusable resource.
3
+
4
+ A skill card captures "how to do X here": intent patterns, a preferred URI flow, ranked
5
+ fallbacks, postconditions, risk, and success stats. Before planning from scratch, the
6
+ system searches skills — "I have a known-good path for this; try it, use the LLM only to
7
+ adapt." Cards store PROCEDURES (transferable), never one-off answers.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import time
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ def _path() -> Path:
19
+ d = Path(os.environ.get("URIRUN_MIND_DIR") or "~/.urirun/mind").expanduser()
20
+ d.mkdir(parents=True, exist_ok=True)
21
+ return d / "skills.json"
22
+
23
+
24
+ def _load() -> dict[str, dict]:
25
+ p = _path()
26
+ if not p.is_file():
27
+ return {}
28
+ try:
29
+ return json.loads(p.read_text(encoding="utf-8"))
30
+ except Exception: # noqa: BLE001
31
+ return {}
32
+
33
+
34
+ def _save(cards: dict) -> None:
35
+ _path().write_text(json.dumps(cards, indent=1, default=str), encoding="utf-8")
36
+
37
+
38
+ def create(card: dict) -> dict[str, Any]:
39
+ """Create/replace a skill card. Requires id + preferred_flow."""
40
+ cid = card.get("id")
41
+ if not cid or not card.get("preferred_flow"):
42
+ raise ValueError("skill card needs 'id' and 'preferred_flow'")
43
+ cards = _load()
44
+ existing = cards.get(cid, {})
45
+ cards[cid] = {
46
+ "id": cid,
47
+ "intent_patterns": card.get("intent_patterns") or existing.get("intent_patterns") or [],
48
+ "preconditions": card.get("preconditions") or [],
49
+ "preferred_flow": card["preferred_flow"],
50
+ "fallbacks": card.get("fallbacks") or [],
51
+ "postconditions": card.get("postconditions") or [],
52
+ "risk": card.get("risk") or {"level": "low", "requires_human_approval": False},
53
+ "stats": existing.get("stats") or {"success_count": 0, "failure_count": 0, "last_success": None},
54
+ }
55
+ _save(cards)
56
+ return cards[cid]
57
+
58
+
59
+ def search(intent_or_prompt: str) -> list[dict]:
60
+ """Cards whose id or intent patterns match the intent/prompt, best (most successful) first."""
61
+ q = (intent_or_prompt or "").lower()
62
+ hits = []
63
+ for card in _load().values():
64
+ if card["id"] in q or q in card["id"] or \
65
+ any(p.lower() in q or q in p.lower() for p in card.get("intent_patterns", [])):
66
+ hits.append(card)
67
+ hits.sort(key=lambda c: c["stats"]["success_count"] - c["stats"]["failure_count"], reverse=True)
68
+ return hits
69
+
70
+
71
+ def show(card_id: str) -> dict | None:
72
+ return _load().get(card_id)
73
+
74
+
75
+ def record_outcome(card_id: str, ok: bool, *, clock: float | None = None) -> dict | None:
76
+ """Update a card's success/failure stats after a run (feeds ranking + trust)."""
77
+ cards = _load()
78
+ card = cards.get(card_id)
79
+ if not card:
80
+ return None
81
+ key = "success_count" if ok else "failure_count"
82
+ card["stats"][key] = card["stats"].get(key, 0) + 1
83
+ if ok:
84
+ card["stats"]["last_success"] = clock if clock is not None else time.time()
85
+ _save(cards)
86
+ return card
87
+
88
+
89
+ def deprecate(card_id: str) -> bool:
90
+ cards = _load()
91
+ if card_id in cards:
92
+ cards[card_id]["risk"] = {**cards[card_id].get("risk", {}), "deprecated": True}
93
+ _save(cards)
94
+ return True
95
+ return False
@@ -0,0 +1,74 @@
1
+ # Author: Tom Sapletta · Part of the ifURI solution.
2
+ """Strategy selector — known-good replay BEFORE the LLM, gated by autonomy + antipatterns.
3
+
4
+ Order (cheap → expensive):
5
+ 1. retrieve a known-good SKILL for this intent; if env fingerprint matches, replay it
6
+ 2. filter out routes an ANTIPATTERN says to avoid for this intent/node status
7
+ 3. otherwise fall through to a fresh plan (the reasoner / LLM)
8
+ Every chosen plan is gated by the AUTONOMY policy before it is called runnable. This cuts
9
+ cost and flakiness and stops the system re-walking failed paths.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from . import antipatterns, autonomy_policy, episode_store, skill_cards
16
+
17
+
18
+ def select(intent: str, *, prompt: str = "", environment: dict | None = None,
19
+ level: int = 3) -> dict[str, Any]:
20
+ """Pick how to satisfy an intent. Returns the source (skill|episode|fresh), the flow,
21
+ the autonomy gate verdict, and any antipattern warnings — WITHOUT calling an LLM."""
22
+ env = environment or {}
23
+ fp = episode_store.env_fingerprint(env)
24
+ node_status = env.get("node_status")
25
+ anti = antipatterns.match(intent, node_status)
26
+
27
+ # 1. known-good skill for this intent (prefer env match)
28
+ cards = skill_cards.search(prompt or intent)
29
+ for card in cards:
30
+ if card.get("risk", {}).get("deprecated"):
31
+ continue
32
+ flow = [{"uri": u} if isinstance(u, str) else u for u in card["preferred_flow"]]
33
+ flow, warns = _drop_avoided(flow, anti)
34
+ if flow:
35
+ gate = autonomy_policy.gate_plan(flow, level=level)
36
+ return _result("skill", card["id"], flow, gate, anti, warns,
37
+ note="known-good skill; adapt with LLM only if env differs")
38
+
39
+ # 2. a successful past episode with the same fingerprint → replay its flow
40
+ for ep in episode_store.similar(intent, fp, result="ok"):
41
+ if ep.get("flow"):
42
+ flow = [{"uri": u} if isinstance(u, str) else u for u in ep["flow"]]
43
+ flow, warns = _drop_avoided(flow, anti)
44
+ if flow:
45
+ gate = autonomy_policy.gate_plan(flow, level=level)
46
+ return _result("episode", ep["id"], flow, gate, anti, warns,
47
+ note="replaying a known-good episode (same env fingerprint)")
48
+
49
+ # 3. no memory → the caller should plan fresh (reasoner/LLM), still under the gate
50
+ return {"source": "fresh", "intent": intent, "fingerprint": fp, "flow": [],
51
+ "antipatterns": [a["id"] for a in anti], "avoid_routes": _avoid_routes(anti),
52
+ "note": "no known-good path; plan fresh (reasoner/LLM) then gate + record the episode",
53
+ "autonomy_level": level}
54
+
55
+
56
+ def _drop_avoided(flow: list[dict], anti: list[dict]) -> tuple[list[dict], list[str]]:
57
+ kept, warns = [], []
58
+ for step in flow:
59
+ rid = antipatterns.is_avoided(step.get("uri", ""), anti)
60
+ if rid:
61
+ warns.append(f"{step.get('uri')} avoided by antipattern {rid}")
62
+ else:
63
+ kept.append(step)
64
+ return kept, warns
65
+
66
+
67
+ def _avoid_routes(anti: list[dict]) -> list[str]:
68
+ return [g for r in anti for g in r.get("avoid", [])]
69
+
70
+
71
+ def _result(source, ref, flow, gate, anti, warns, note) -> dict:
72
+ return {"source": source, "ref": ref, "flow": flow, "gate": gate,
73
+ "antipatterns": [a["id"] for a in anti], "warnings": warns,
74
+ "runnable": gate["runnable"], "note": note}
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: urirun-mind
3
+ Version: 0.1.0
4
+ Summary: Cognitive layer over connectors: episode memory, skill cards, antipatterns, capability graph, autonomy levels, strategy selection, reflection. Learn procedures not answers.
5
+ Author: Tom Sapletta
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+
10
+ # urirun-mind
11
+
12
+ The cognitive layer over connectors — **operational self-awareness**. Not human
13
+ consciousness: an operating model of self. *I know what I want, what I have, what worked
14
+ before, what I can't do, when to build/repair a capability, when to ask a human, and how
15
+ to measure real success.*
16
+
17
+ **Learn procedures, not answers.** Store *"for office tasks on a stale node, go headless
18
+ first"* — never *"LibreOffice missing on lenovo"*. Procedures transfer across nodes and tasks.
19
+
20
+ | module | role |
21
+ |---|---|
22
+ | `episode_store` | record every task as an episode, keyed by an **environment fingerprint**; retrieve similar (known-good first) |
23
+ | `skill_cards` | promote a working flow to a reusable **procedure**; search by intent; track success stats |
24
+ | `antipatterns` | learn from counter-examples — routes to **avoid** for an intent/node-status, and what to prefer |
25
+ | `capability_graph` | capability → required sub-capabilities + **fallback chains** (no youtube:// ≠ dead end) |
26
+ | `autonomy_policy` | **L0 observe → L5 commit**; gate each action; never auto for pay/publish/send/delete/credentials |
27
+ | `strategy_selector` | **known-good replay BEFORE the LLM** — skill → episode → fresh, minus antipattern routes, under the autonomy gate |
28
+ | `reflection` | after a run: silent-failure check, promote skill, record antipattern, emit **koru tickets** |
29
+
30
+ ## The loop it closes
31
+ ```
32
+ run → reflect → learn → ticket → koru → connector/code → smoke → publish → use
33
+ ```
34
+ Round 1 of a task plans fresh; the failure teaches an antipattern + a procedure lesson.
35
+ Round 2 of the same task replays the known-good skill and avoids the failed path — cheaper,
36
+ less flaky, and honestly gated by autonomy level.
37
+
38
+ Part of the ifURI solution · Author: Tom Sapletta · Apache-2.0
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_mind.py
4
+ urirun_mind/__init__.py
5
+ urirun_mind/antipatterns.py
6
+ urirun_mind/autonomy_policy.py
7
+ urirun_mind/capability_graph.py
8
+ urirun_mind/cli.py
9
+ urirun_mind/episode_store.py
10
+ urirun_mind/reflection.py
11
+ urirun_mind/skill_cards.py
12
+ urirun_mind/strategy_selector.py
13
+ urirun_mind.egg-info/PKG-INFO
14
+ urirun_mind.egg-info/SOURCES.txt
15
+ urirun_mind.egg-info/dependency_links.txt
16
+ urirun_mind.egg-info/entry_points.txt
17
+ urirun_mind.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ urirun-mind = urirun_mind.cli:main
@@ -0,0 +1 @@
1
+ urirun_mind