robo-cortex 0.4.0__py3-none-any.whl

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.
Files changed (48) hide show
  1. robo_cortex/__init__.py +5 -0
  2. robo_cortex/cli/__init__.py +5 -0
  3. robo_cortex/cli/__main__.py +6 -0
  4. robo_cortex/cli/_common.py +72 -0
  5. robo_cortex/cli/_output.py +76 -0
  6. robo_cortex/cli/app.py +74 -0
  7. robo_cortex/cli/commands/__init__.py +36 -0
  8. robo_cortex/cli/commands/affected.py +40 -0
  9. robo_cortex/cli/commands/completion.py +94 -0
  10. robo_cortex/cli/commands/evidence.py +110 -0
  11. robo_cortex/cli/commands/hooks.py +149 -0
  12. robo_cortex/cli/commands/init.py +76 -0
  13. robo_cortex/cli/commands/link.py +47 -0
  14. robo_cortex/cli/commands/list_cmd.py +57 -0
  15. robo_cortex/cli/commands/mcp.py +37 -0
  16. robo_cortex/cli/commands/record.py +156 -0
  17. robo_cortex/cli/commands/retrieve.py +88 -0
  18. robo_cortex/cli/commands/search.py +48 -0
  19. robo_cortex/cli/commands/show.py +62 -0
  20. robo_cortex/cli/commands/status.py +83 -0
  21. robo_cortex/cli/commands/transfer.py +140 -0
  22. robo_cortex/core/__init__.py +0 -0
  23. robo_cortex/core/assumptions.py +39 -0
  24. robo_cortex/core/db.py +67 -0
  25. robo_cortex/core/errors.py +32 -0
  26. robo_cortex/core/evidence.py +166 -0
  27. robo_cortex/core/git.py +162 -0
  28. robo_cortex/core/hooks.py +214 -0
  29. robo_cortex/core/init.py +75 -0
  30. robo_cortex/core/invalidate.py +436 -0
  31. robo_cortex/core/lifecycle.py +165 -0
  32. robo_cortex/core/memory.py +394 -0
  33. robo_cortex/core/retrieve.py +429 -0
  34. robo_cortex/core/semantic.py +48 -0
  35. robo_cortex/core/store.py +72 -0
  36. robo_cortex/core/text.py +27 -0
  37. robo_cortex/core/transfer.py +246 -0
  38. robo_cortex/gitea.py +140 -0
  39. robo_cortex/mcp_server.py +263 -0
  40. robo_cortex/migrations/0001_init.sql +101 -0
  41. robo_cortex/migrations/0002_add_usage_tracking.sql +2 -0
  42. robo_cortex/migrations/0003_add_meta_table.sql +4 -0
  43. robo_cortex/sdk.py +196 -0
  44. robo_cortex-0.4.0.dist-info/METADATA +367 -0
  45. robo_cortex-0.4.0.dist-info/RECORD +48 -0
  46. robo_cortex-0.4.0.dist-info/WHEEL +4 -0
  47. robo_cortex-0.4.0.dist-info/entry_points.txt +3 -0
  48. robo_cortex-0.4.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,76 @@
1
+ """Command: roco init"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.init import init_repo
7
+ from robo_cortex.core.errors import RoboCortexError
8
+ from robo_cortex.core.store import open_global_store
9
+ from robo_cortex.cli._common import _get_cmd_name, cli_command
10
+
11
+
12
+ @cli_command("init")
13
+ def run(args) -> int:
14
+ if args.use_global:
15
+ return _run_global(args)
16
+
17
+ result = init_repo(args.repo)
18
+
19
+ if args.json:
20
+ print(json.dumps(result))
21
+ else:
22
+ print(f"Initialized robo-cortex in {result['db_path']}")
23
+ if result["gitignore_updated"]:
24
+ print(f"Added .cortex/ to {result['repo_root']}/.gitignore")
25
+ if "warning" in result:
26
+ print(f"warning: {result['warning']}", file=sys.stderr)
27
+ return 0
28
+
29
+
30
+ def _run_global(args) -> int:
31
+ """`roco init --global`: explicitly create/open the scope-B store
32
+ (~/.cortex/global.db, ARCHITECTURE.md §2), instead of leaving it to be
33
+ created implicitly by the first `record --scope global` call. No git
34
+ repository is needed or resolved -- the global store lives outside any
35
+ single repo by construction.
36
+ """
37
+ conn = open_global_store()
38
+ if conn is None:
39
+ print(
40
+ f"{_get_cmd_name()} init --global: global store is disabled "
41
+ "(ROBO_CORTEX_NO_GLOBAL is set)",
42
+ file=sys.stderr,
43
+ )
44
+ return 1
45
+ db_path = conn.execute("PRAGMA database_list").fetchone()[2]
46
+ conn.close()
47
+
48
+ if args.json:
49
+ print(json.dumps({"db_path": db_path}))
50
+ else:
51
+ print(f"Initialized robo-cortex global store in {db_path}")
52
+ print(
53
+ "Global memories (--scope global) require --assumptions: "
54
+ "conditions that must match the task text verbatim before the "
55
+ "memory is retrieved (ARCHITECTURE.md §5.4)."
56
+ )
57
+ return 0
58
+
59
+
60
+ def register(subparsers):
61
+ p = subparsers.add_parser(
62
+ "init", help="Initialize robo-cortex in a git repository"
63
+ )
64
+ p.add_argument(
65
+ "--repo",
66
+ default=None,
67
+ help="Path inside the target repository (default: current directory)",
68
+ )
69
+ p.add_argument(
70
+ "--global", dest="use_global", action="store_true",
71
+ help="Initialize the global (scope=global) store instead (~/.cortex/global.db); no git repository needed",
72
+ )
73
+ p.add_argument(
74
+ "--json", action="store_true", help="Machine-readable JSON output"
75
+ )
76
+ p.set_defaults(func=run)
@@ -0,0 +1,47 @@
1
+ """Command: roco link"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.lifecycle import create_link
7
+ from robo_cortex.core.memory import find_memory_store
8
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, LINK_TYPE_ARGS, cli_command
9
+
10
+
11
+ @cli_command("link")
12
+ def run(args) -> int:
13
+ link_type = LINK_TYPE_ARGS[args.link_type]
14
+ with _store(args.repo) as (_repo_root, conn), _global_store() as global_conn:
15
+ store1 = find_memory_store(conn, global_conn, args.id1, scope=args.scope)
16
+ store2 = find_memory_store(conn, global_conn, args.id2, scope=args.scope)
17
+ if store1 is not store2:
18
+ print(
19
+ "robo-cortex link: cannot link memories across the repo "
20
+ "and global stores (memory_link is a same-store reference)",
21
+ file=sys.stderr,
22
+ )
23
+ return 1
24
+ result = create_link(store1, args.id1, args.id2, link_type)
25
+
26
+ if args.json:
27
+ print(json.dumps(result))
28
+ else:
29
+ print(f"Linked {result['from_id']} --{result['link_type']}--> {result['to_id']}")
30
+ return 0
31
+
32
+
33
+ def register(subparsers):
34
+ p = subparsers.add_parser("link", help="Link two memories as contradicting or duplicate")
35
+ p.add_argument("id1", type=int)
36
+ p.add_argument("link_type", choices=sorted(LINK_TYPE_ARGS))
37
+ p.add_argument("id2", type=int)
38
+ p.add_argument("--repo", default=None)
39
+ p.add_argument(
40
+ "--scope",
41
+ choices=["repo", "global"],
42
+ default=None,
43
+ help="Disambiguate ids that collide between the repo and global stores "
44
+ "(both id1 and id2 must be in the same store to link)",
45
+ )
46
+ p.add_argument("--json", action="store_true")
47
+ p.set_defaults(func=run)
@@ -0,0 +1,57 @@
1
+ """Command: roco list"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.memory import list_memories
7
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
8
+ from robo_cortex.cli._output import format_status
9
+
10
+
11
+ @cli_command("list")
12
+ def run(args) -> int:
13
+ with _store(args.repo) as (_repo_root, conn), _global_store() as global_conn:
14
+ results = []
15
+ if args.scope in (None, "repo"):
16
+ results += list_memories(
17
+ conn,
18
+ status=args.status,
19
+ scope=args.scope,
20
+ type=args.type,
21
+ needs_consolidation=args.needs_consolidation,
22
+ abandoned_without_lesson=args.abandoned_without_lesson,
23
+ )
24
+ if args.scope in (None, "global"):
25
+ results += list_memories(
26
+ global_conn,
27
+ status=args.status,
28
+ scope=args.scope,
29
+ type=args.type,
30
+ needs_consolidation=args.needs_consolidation,
31
+ abandoned_without_lesson=args.abandoned_without_lesson,
32
+ )
33
+ results.sort(key=lambda m: m["id"])
34
+
35
+ if args.json:
36
+ print(json.dumps(results))
37
+ else:
38
+ if not results:
39
+ print("No memories found.")
40
+ for memory in results:
41
+ print(
42
+ f"[{memory['id']}] {memory['type']:12s} {memory['scope']:6s} "
43
+ f"{format_status(memory['status'], width=13)} {memory['statement']}"
44
+ )
45
+ return 0
46
+
47
+
48
+ def register(subparsers):
49
+ p = subparsers.add_parser("list", help="List memories")
50
+ p.add_argument("--repo", default=None)
51
+ p.add_argument("--status", default=None)
52
+ p.add_argument("--scope", choices=["repo", "global"], default=None)
53
+ p.add_argument("--type", default=None)
54
+ p.add_argument("--needs-consolidation", action="store_true")
55
+ p.add_argument("--abandoned-without-lesson", action="store_true")
56
+ p.add_argument("--json", action="store_true")
57
+ p.set_defaults(func=run)
@@ -0,0 +1,37 @@
1
+ """Command: roco mcp"""
2
+
3
+ import sys
4
+
5
+ from robo_cortex.core.errors import RoboCortexError
6
+ from robo_cortex.cli._common import _get_cmd_name, cli_command
7
+
8
+
9
+ @cli_command("mcp")
10
+ def run(args) -> int:
11
+ # Lazy import: the mcp SDK is only needed for this one subcommand, not
12
+ # for every CLI invocation -- keeps `robo-cortex record`/`show`/etc.
13
+ # fast and independent of an mcp package version, matching the mission's
14
+ # "every third-party dependency must be justified" posture (justified
15
+ # exactly here, nowhere else).
16
+ try:
17
+ from robo_cortex.mcp_server import run as run_mcp_server
18
+ except ImportError:
19
+ print(
20
+ f"{_get_cmd_name()} mcp: the 'mcp' package is not installed. "
21
+ f"Install it with: pip install 'robo-cortex[mcp]'",
22
+ file=sys.stderr,
23
+ )
24
+ return 1
25
+
26
+ run_mcp_server(args.repo)
27
+ return 0
28
+
29
+
30
+ def register(subparsers):
31
+ p = subparsers.add_parser("mcp", help="Run the MCP server (stdio)")
32
+ p.add_argument(
33
+ "--repo",
34
+ default=None,
35
+ help="Path inside the target repository (default: current directory)",
36
+ )
37
+ p.set_defaults(func=run)
@@ -0,0 +1,156 @@
1
+ """Command: roco record"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.memory import (
7
+ CONFIDENCES,
8
+ SCOPES,
9
+ TYPES,
10
+ record_batch,
11
+ record_memory,
12
+ )
13
+ from robo_cortex.core.invalidate import add_path, update_path
14
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
15
+
16
+
17
+ @cli_command("record")
18
+ def run(args) -> int:
19
+ with _store(args.repo) as (repo_root, conn):
20
+ if args.update_path:
21
+ if not (args.id and args.old_path and args.new_path):
22
+ print(
23
+ "robo-cortex record --update-path: ID, --old-path, "
24
+ "and --new-path are all required",
25
+ file=sys.stderr,
26
+ )
27
+ return 2
28
+ result = update_path(conn, repo_root, args.id, args.old_path, args.new_path)
29
+ if args.json:
30
+ print(json.dumps(result))
31
+ else:
32
+ print(f"Updated memory {result['id']}: {args.old_path} -> {args.new_path}")
33
+ return 0
34
+
35
+ if args.add_path:
36
+ if not args.id:
37
+ print(
38
+ "robo-cortex record --add-path: ID is required",
39
+ file=sys.stderr,
40
+ )
41
+ return 2
42
+ result = add_path(conn, repo_root, args.id, args.add_path)
43
+ if args.json:
44
+ print(json.dumps(result))
45
+ else:
46
+ print(f"Added path to memory {result['id']}: {args.add_path}")
47
+ return 0
48
+
49
+ if args.batch:
50
+ lines = sys.stdin.read().splitlines()
51
+ result = record_batch(conn, repo_root, lines)
52
+ if args.json:
53
+ print(json.dumps(result))
54
+ else:
55
+ print(f"Created {result['created']} memories.")
56
+ for failure in result["failed"]:
57
+ print(
58
+ f" line {failure['line']}: {failure['error']}",
59
+ file=sys.stderr,
60
+ )
61
+ return 0
62
+
63
+ if not (args.type and args.scope and args.statement and args.confidence):
64
+ print(
65
+ "robo-cortex record: --type, --scope, --statement, and "
66
+ "--confidence are required (or use --batch to read JSON "
67
+ "Lines from stdin, --update-path to relink an existing "
68
+ "memory, or --add-path to attach a path to one)",
69
+ file=sys.stderr,
70
+ )
71
+ return 2
72
+
73
+ if args.scope == "global":
74
+ # scope='global' memories live in ~/.cortex/global.db, not
75
+ # the local repo store -- ARCHITECTURE.md §2: scope B "cannot
76
+ # live in one repository's .cortex/".
77
+ with _global_store() as global_conn:
78
+ result = record_memory(
79
+ global_conn,
80
+ repo_root,
81
+ type=args.type,
82
+ scope=args.scope,
83
+ statement=args.statement,
84
+ confidence=args.confidence,
85
+ why_it_matters=args.why_it_matters,
86
+ assumptions=args.assumptions,
87
+ paths=args.paths,
88
+ lesson_from=args.lesson_from,
89
+ )
90
+ else:
91
+ result = record_memory(
92
+ conn,
93
+ repo_root,
94
+ type=args.type,
95
+ scope=args.scope,
96
+ statement=args.statement,
97
+ confidence=args.confidence,
98
+ why_it_matters=args.why_it_matters,
99
+ assumptions=args.assumptions,
100
+ paths=args.paths,
101
+ lesson_from=args.lesson_from,
102
+ )
103
+
104
+ if args.json:
105
+ print(json.dumps(result))
106
+ else:
107
+ print(f"Recorded memory {result['id']} (status: {result['status']})")
108
+ for path in result["paths"]:
109
+ print(f" path: {path['path']} blob_hash: {path['blob_hash']}")
110
+ return 0
111
+
112
+
113
+ def register(subparsers):
114
+ p = subparsers.add_parser(
115
+ "record",
116
+ help="Record a new memory",
117
+ description="Record a new memory in this repository or globally.\n"
118
+ "New memories start in 'provisional' status. Use 'roco status <id> activate' to promote to 'active', "
119
+ "or attach evidence which auto-promotes provisional→active on the first evidence.",
120
+ )
121
+ p.add_argument("id", nargs="?", type=int, default=None, help="Used with --update-path or --add-path")
122
+ p.add_argument("--repo", default=None)
123
+ p.add_argument("--type", choices=sorted(TYPES), default=None, help="Memory type: decision, lesson, fact, etc.")
124
+ p.add_argument("--scope", choices=sorted(SCOPES), default=None, help="Memory scope: 'repo' (this project) or 'global' (reusable)")
125
+ p.add_argument("--statement", default=None)
126
+ p.add_argument(
127
+ "--confidence", choices=sorted(CONFIDENCES), default=None,
128
+ help="Confidence level: low, medium, or high"
129
+ )
130
+ p.add_argument("--why", dest="why_it_matters", default=None)
131
+ p.add_argument("--assumptions", default=None, help="Comma-separated preconditions (for global memories, all must match the task text verbatim)")
132
+ p.add_argument(
133
+ "--path", dest="paths", action="append", default=[]
134
+ )
135
+ p.add_argument(
136
+ "--lesson-from", dest="lesson_from", type=int, default=None,
137
+ help="Link a new type=lesson memory back to the abandoned memory it compresses",
138
+ )
139
+ p.add_argument(
140
+ "--batch",
141
+ action="store_true",
142
+ help="Read JSON Lines from stdin instead of the flags above",
143
+ )
144
+ p.add_argument(
145
+ "--update-path", action="store_true",
146
+ help="Relink an existing memory's path (needs ID, --old-path, --new-path)",
147
+ )
148
+ p.add_argument("--old-path", default=None)
149
+ p.add_argument("--new-path", default=None)
150
+ p.add_argument(
151
+ "--add-path", dest="add_path", default=None,
152
+ help="Attach a new path to an existing memory (needs ID and --add-path); "
153
+ "useful for a file that didn't exist at HEAD when the memory was first recorded",
154
+ )
155
+ p.add_argument("--json", action="store_true")
156
+ p.set_defaults(func=run)
@@ -0,0 +1,88 @@
1
+ """Command: roco retrieve"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.retrieve import (
7
+ DEFAULT_BUDGET_ITEMS,
8
+ DEFAULT_BUDGET_TOKENS,
9
+ retrieve_context,
10
+ )
11
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
12
+ from robo_cortex.cli._output import _format_score_breakdown, format_status
13
+
14
+
15
+ @cli_command("retrieve")
16
+ def run(args) -> int:
17
+ if not args.task:
18
+ print("robo-cortex retrieve: --task is required", file=sys.stderr)
19
+ return 2
20
+
21
+ with _store(args.repo) as (repo_root, conn), _global_store() as global_conn:
22
+ result = retrieve_context(
23
+ conn,
24
+ repo_root,
25
+ task=args.task,
26
+ paths=args.paths,
27
+ budget_items=args.budget_items,
28
+ budget_tokens=args.budget_tokens,
29
+ explain=args.explain,
30
+ global_conn=global_conn,
31
+ )
32
+
33
+ if args.json:
34
+ print(json.dumps(result))
35
+ else:
36
+ meta = result["meta"]
37
+ print(
38
+ f"matched={meta['matched']} returned={meta['returned']} "
39
+ f"needs_review={meta['needs_review']} contradicted={meta['contradicted']}"
40
+ )
41
+ if meta["matched"] == 0:
42
+ print(
43
+ " hint: no memory passed candidacy (global-scope items must pass"
44
+ )
45
+ print(
46
+ " the assumptions gate — see ARCHITECTURE.md §5.4). Try:"
47
+ )
48
+ print(f" roco search --query \"{args.task}\"")
49
+ print(
50
+ " roco show <id> --explain-against \"<task>\" (see why candidates didn't match)"
51
+ )
52
+ for omission in meta["omitted"]:
53
+ print(f" omitted: {omission['count']} ({omission['reason']})")
54
+ if args.explain and "omitted_details" in meta:
55
+ for detail in meta["omitted_details"]:
56
+ print(f" - id={detail['id']} scope={detail['scope']} omitted due to {detail['reason']}")
57
+ for item in result["data"]:
58
+ # Status is only surfaced when it's not the common case (active):
59
+ # a needs_review item buried in a ranked result list is worth
60
+ # flagging inline, not just visible via the aggregate count above.
61
+ status_marker = (
62
+ f"{format_status(item['status'])} " if item["status"] != "active" else ""
63
+ )
64
+ print(
65
+ f"[{item['id']}] score={item['score']:.3f} "
66
+ f"{item['scope']:6s} {item['type']:12s} {status_marker}{item['statement']}"
67
+ )
68
+ if args.explain:
69
+ print(f" {_format_score_breakdown(item['score_breakdown'])}")
70
+ return 0
71
+
72
+
73
+ def register(subparsers):
74
+ p = subparsers.add_parser(
75
+ "retrieve", help="Retrieve a ranked, budgeted context pack for a task"
76
+ )
77
+ p.add_argument("--repo", default=None)
78
+ p.add_argument("--task", default=None)
79
+ p.add_argument("--path", dest="paths", action="append", default=[])
80
+ p.add_argument(
81
+ "--budget-items", type=int, default=DEFAULT_BUDGET_ITEMS
82
+ )
83
+ p.add_argument(
84
+ "--budget-tokens", type=int, default=DEFAULT_BUDGET_TOKENS
85
+ )
86
+ p.add_argument("--explain", action="store_true")
87
+ p.add_argument("--json", action="store_true")
88
+ p.set_defaults(func=run)
@@ -0,0 +1,48 @@
1
+ """Command: roco search"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.retrieve import DEFAULT_SEARCH_LIMIT, search_memory
7
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
8
+
9
+
10
+ @cli_command("search")
11
+ def run(args) -> int:
12
+ if not args.query:
13
+ print("robo-cortex search: --query is required", file=sys.stderr)
14
+ return 2
15
+
16
+ with _store(args.repo) as (repo_root, conn), _global_store() as global_conn:
17
+ result = search_memory(
18
+ conn,
19
+ repo_root,
20
+ query=args.query,
21
+ scope=args.scope,
22
+ type=args.type,
23
+ status=args.status,
24
+ limit=args.limit,
25
+ global_conn=global_conn,
26
+ )
27
+
28
+ if args.json:
29
+ print(json.dumps(result))
30
+ else:
31
+ print(f"matched={result['matched']} returned={result['returned']}")
32
+ for item in result["data"]:
33
+ print(f"[{item['id']}] {item['type']:12s} {item['status']:13s} {item['statement']}")
34
+ return 0
35
+
36
+
37
+ def register(subparsers):
38
+ p = subparsers.add_parser(
39
+ "search", help="Search memories by area, task, or concept (exploration, not budgeted)"
40
+ )
41
+ p.add_argument("--repo", default=None)
42
+ p.add_argument("--query", default=None)
43
+ p.add_argument("--scope", default=None)
44
+ p.add_argument("--type", default=None)
45
+ p.add_argument("--status", default=None)
46
+ p.add_argument("--limit", type=int, default=DEFAULT_SEARCH_LIMIT)
47
+ p.add_argument("--json", action="store_true")
48
+ p.set_defaults(func=run)
@@ -0,0 +1,62 @@
1
+ """Command: roco show"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.memory import find_memory_store, get_memory
7
+ from robo_cortex.core.retrieve import explain_memory_score
8
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
9
+ from robo_cortex.cli._output import _format_score_breakdown, format_status
10
+
11
+
12
+ @cli_command("show")
13
+ def run(args) -> int:
14
+ with _store(args.repo) as (_repo_root, conn), _global_store() as global_conn:
15
+ store = find_memory_store(conn, global_conn, args.id, scope=args.scope)
16
+ result = get_memory(store, args.id)
17
+ if args.explain_against:
18
+ result["score_breakdown"] = explain_memory_score(
19
+ conn, args.id, args.explain_against,
20
+ global_conn=global_conn, scope=args.scope,
21
+ )
22
+
23
+ if args.json:
24
+ print(json.dumps(result))
25
+ else:
26
+ print(
27
+ f"[{result['id']}] {result['type']} "
28
+ f"({result['scope']}, {format_status(result['status'])}, "
29
+ f"confidence={result['confidence']})"
30
+ )
31
+ print(result["statement"])
32
+ if result["status_reason"]:
33
+ print(f"status reason: {result['status_reason']}")
34
+ if result["why_it_matters"]:
35
+ print(f"why it matters: {result['why_it_matters']}")
36
+ if result["assumptions"]:
37
+ print(f"assumptions: {result['assumptions']}")
38
+ for path in result["paths"]:
39
+ print(f" path: {path['path']} blob_hash: {path['blob_hash']}")
40
+ for item in result["evidence"]:
41
+ print(f" evidence [{item['id']}] {item['kind']} ({item['status']}): {item['description']}")
42
+ for link in result["links"]:
43
+ print(f" link: {link['direction']} {link['link_type']} <-> memory {link['memory_id']}")
44
+ if "score_breakdown" in result:
45
+ print(f" explain: {_format_score_breakdown(result['score_breakdown'])}")
46
+ return 0
47
+
48
+
49
+ def register(subparsers):
50
+ p = subparsers.add_parser("show", help="Show one memory in full")
51
+ p.add_argument("id", type=int)
52
+ p.add_argument("--repo", default=None)
53
+ p.add_argument(
54
+ "--scope", choices=["repo", "global"], default=None,
55
+ help="Disambiguate an id that collides between the repo and global stores",
56
+ )
57
+ p.add_argument(
58
+ "--explain-against", dest="explain_against", default=None,
59
+ help="Print this memory's score components against a hypothetical task",
60
+ )
61
+ p.add_argument("--json", action="store_true")
62
+ p.set_defaults(func=run)
@@ -0,0 +1,83 @@
1
+ """Command: roco status"""
2
+
3
+ import json
4
+ import sys
5
+
6
+ from robo_cortex.core.lifecycle import STATUS_ACTIONS, change_status, status_batch
7
+ from robo_cortex.core.memory import find_memory_store
8
+ from robo_cortex.cli._common import _get_cmd_name, _store, _global_store, cli_command
9
+
10
+
11
+ @cli_command("status")
12
+ def run(args) -> int:
13
+ with _store(args.repo) as (repo_root, conn), _global_store() as global_conn:
14
+ if args.batch:
15
+ lines = sys.stdin.read().splitlines()
16
+ result = status_batch(conn, global_conn, repo_root, lines)
17
+ if args.json:
18
+ print(json.dumps(result))
19
+ else:
20
+ print(f"Updated {result['updated']} memories.")
21
+ for failure in result["failed"]:
22
+ print(
23
+ f" line {failure['line']}: {failure['error']}",
24
+ file=sys.stderr,
25
+ )
26
+ return 0
27
+
28
+ if not args.id or not args.action:
29
+ print(
30
+ "robo-cortex status: ID and ACTION are required "
31
+ "(or use --batch to read JSON Lines from stdin)",
32
+ file=sys.stderr,
33
+ )
34
+ return 2
35
+
36
+ if not args.reason:
37
+ print("robo-cortex status: --reason is required", file=sys.stderr)
38
+ return 2
39
+
40
+ new_status = STATUS_ACTIONS[args.action]
41
+ store = find_memory_store(conn, global_conn, args.id, scope=args.scope)
42
+ result = change_status(
43
+ store, repo_root, args.id, new_status, args.reason,
44
+ supersedes_link_to=args.supersedes,
45
+ )
46
+
47
+ if args.json:
48
+ print(json.dumps(result))
49
+ else:
50
+ print(f"Memory {result['id']} -> {result['status']}")
51
+ return 0
52
+
53
+
54
+ def register(subparsers):
55
+ p = subparsers.add_parser("status", help="Change a memory's status")
56
+ p.add_argument("id", type=int, nargs="?", default=None)
57
+ p.add_argument(
58
+ "action",
59
+ nargs="?",
60
+ choices=sorted(STATUS_ACTIONS),
61
+ default=None,
62
+ help="Verb: activate, supersede, invalidate, abandon, or archive (state transitions—ARCHITECTURE.md §4)",
63
+ )
64
+ p.add_argument(
65
+ "--reason",
66
+ default=None,
67
+ help="Required: explain why this status change (permanent audit trail, not just current state)",
68
+ )
69
+ p.add_argument("--supersedes", type=int, default=None)
70
+ p.add_argument("--repo", default=None)
71
+ p.add_argument(
72
+ "--scope",
73
+ choices=["repo", "global"],
74
+ default=None,
75
+ help="Disambiguate an id that collides between the repo and global stores",
76
+ )
77
+ p.add_argument(
78
+ "--batch",
79
+ action="store_true",
80
+ help="Read JSON Lines (id, action, reason, ...) from stdin instead of single id/action",
81
+ )
82
+ p.add_argument("--json", action="store_true")
83
+ p.set_defaults(func=run)