rememberstack 0.1.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 (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,110 @@
1
+ """Agent-facing surfaces, imported lazily to keep the client wheel light."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from typing import Any
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from rememberstack.model.client import ToolDescriptor as ToolDescriptor
11
+ from rememberstack.surfaces.cli import main as cli_main # noqa: F401
12
+ from rememberstack.surfaces.consumption_skill import (
13
+ ConsumptionSkillSurface as ConsumptionSkillSurface,
14
+ )
15
+ from rememberstack.surfaces.graph_queries import GraphQueries as GraphQueries
16
+ from rememberstack.surfaces.http_api import build_api as build_api
17
+ from rememberstack.surfaces.mcp import RecipeMcpServer as RecipeMcpServer
18
+ from rememberstack.surfaces.query_engine import QueryEngine as QueryEngine
19
+ from rememberstack.surfaces.recipe_executor import EXECUTABLE_OPS as EXECUTABLE_OPS
20
+ from rememberstack.surfaces.recipe_executor import (
21
+ RecipeExecutionError as RecipeExecutionError,
22
+ )
23
+ from rememberstack.surfaces.recipe_executor import RecipeExecutor as RecipeExecutor
24
+ from rememberstack.surfaces.recipe_surface import (
25
+ InvalidArgumentError as InvalidArgumentError,
26
+ )
27
+ from rememberstack.surfaces.recipe_surface import (
28
+ MissingArgumentError as MissingArgumentError,
29
+ )
30
+ from rememberstack.surfaces.recipe_surface import RecipeSurface as RecipeSurface
31
+ from rememberstack.surfaces.recipe_surface import (
32
+ UnknownRecipeError as UnknownRecipeError,
33
+ )
34
+ from rememberstack.surfaces.remote_mcp import (
35
+ RemoteRecipeMcpServer as RemoteRecipeMcpServer,
36
+ )
37
+ from rememberstack.surfaces.remote_mcp import serve_mcp_stdio as serve_mcp_stdio
38
+ from rememberstack.surfaces.sdk import MemoryApiError as MemoryApiError
39
+ from rememberstack.surfaces.sdk import MemoryClient as MemoryClient
40
+
41
+ _EXPORTS = {
42
+ "EXECUTABLE_OPS": ("rememberstack.surfaces.recipe_executor", "EXECUTABLE_OPS"),
43
+ "ConsumptionSkillSurface": (
44
+ "rememberstack.surfaces.consumption_skill",
45
+ "ConsumptionSkillSurface",
46
+ ),
47
+ "GraphQueries": ("rememberstack.surfaces.graph_queries", "GraphQueries"),
48
+ "InvalidArgumentError": (
49
+ "rememberstack.surfaces.recipe_surface",
50
+ "InvalidArgumentError",
51
+ ),
52
+ "MemoryApiError": ("rememberstack.surfaces.sdk", "MemoryApiError"),
53
+ "MemoryClient": ("rememberstack.surfaces.sdk", "MemoryClient"),
54
+ "MissingArgumentError": (
55
+ "rememberstack.surfaces.recipe_surface",
56
+ "MissingArgumentError",
57
+ ),
58
+ "QueryEngine": ("rememberstack.surfaces.query_engine", "QueryEngine"),
59
+ "RecipeExecutionError": (
60
+ "rememberstack.surfaces.recipe_executor",
61
+ "RecipeExecutionError",
62
+ ),
63
+ "RecipeExecutor": ("rememberstack.surfaces.recipe_executor", "RecipeExecutor"),
64
+ "RecipeMcpServer": ("rememberstack.surfaces.mcp", "RecipeMcpServer"),
65
+ "RecipeSurface": ("rememberstack.surfaces.recipe_surface", "RecipeSurface"),
66
+ "RemoteRecipeMcpServer": (
67
+ "rememberstack.surfaces.remote_mcp",
68
+ "RemoteRecipeMcpServer",
69
+ ),
70
+ "ToolDescriptor": ("rememberstack.model.client", "ToolDescriptor"),
71
+ "UnknownRecipeError": (
72
+ "rememberstack.surfaces.recipe_surface",
73
+ "UnknownRecipeError",
74
+ ),
75
+ "build_api": ("rememberstack.surfaces.http_api", "build_api"),
76
+ "cli_main": ("rememberstack.surfaces.cli", "main"),
77
+ "serve_mcp_stdio": ("rememberstack.surfaces.remote_mcp", "serve_mcp_stdio"),
78
+ }
79
+
80
+ __all__ = (
81
+ "EXECUTABLE_OPS",
82
+ "ConsumptionSkillSurface",
83
+ "GraphQueries",
84
+ "InvalidArgumentError",
85
+ "MemoryApiError",
86
+ "MemoryClient",
87
+ "MissingArgumentError",
88
+ "QueryEngine",
89
+ "RecipeExecutionError",
90
+ "RecipeExecutor",
91
+ "RecipeMcpServer",
92
+ "RecipeSurface",
93
+ "RemoteRecipeMcpServer",
94
+ "ToolDescriptor",
95
+ "UnknownRecipeError",
96
+ "build_api",
97
+ "cli_main",
98
+ "serve_mcp_stdio",
99
+ )
100
+
101
+
102
+ def __getattr__(name: str) -> Any:
103
+ """Load each surface only when callers request it."""
104
+ try:
105
+ module_name, attribute = _EXPORTS[name]
106
+ except KeyError as error:
107
+ raise AttributeError(name) from error
108
+ value = getattr(import_module(module_name), attribute)
109
+ globals()[name] = value
110
+ return value
@@ -0,0 +1,447 @@
1
+ """The ``remember`` CLI: a dependency-light client plus optional local admin commands.
2
+
3
+ Query, ingest, connector management, and MCP all talk to the deployment HTTP
4
+ API. ``remember review``, ``remember budget``, and ``remember ops`` import the server extra
5
+ and connect to the spine.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ from datetime import datetime
12
+ from importlib import import_module
13
+ import json
14
+ from pathlib import Path
15
+ import sys
16
+ from typing import TYPE_CHECKING
17
+ from uuid import UUID
18
+
19
+ import httpx
20
+ from pydantic import JsonValue
21
+
22
+ from rememberstack import __version__
23
+ from rememberstack.model.adjudication import ReviewDecisionError
24
+ from rememberstack.model.client import ConnectorCreate
25
+ from rememberstack.surfaces.remote_mcp import RemoteRecipeMcpServer
26
+ from rememberstack.surfaces.remote_mcp import serve_mcp_stdio
27
+ from rememberstack.surfaces.sdk import MemoryApiError
28
+ from rememberstack.surfaces.sdk import MemoryClient
29
+
30
+ if TYPE_CHECKING:
31
+ from rememberstack.spine.review import ReviewQueue
32
+ from rememberstack.spine.work_ledger import WorkLedger
33
+
34
+ _MERGE_VERDICTS = ("merge", "not_merge")
35
+ _TRIAGE_VERDICTS = ("restore_support", "invalidate_fact", "uncertain")
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ """The ``remember`` entry point; returns the process exit code."""
40
+ parser = _build_parser()
41
+ args = parser.parse_args(argv)
42
+ try:
43
+ if args.command == "review":
44
+ return _run_review(args)
45
+ if args.command == "budget":
46
+ return _run_budget(args)
47
+ if args.command == "ops":
48
+ return _run_ops(args)
49
+ if args.command == "query":
50
+ return _run_query(args)
51
+ if args.command == "ingest":
52
+ return _run_ingest(args)
53
+ if args.command == "connectors":
54
+ return _run_connectors(args)
55
+ if args.command == "mcp":
56
+ return _run_mcp()
57
+ except MemoryApiError as error:
58
+ print(f"error: {error}", file=sys.stderr)
59
+ return 1
60
+ parser.print_help()
61
+ return 2
62
+
63
+
64
+ def _run_review(args: argparse.Namespace) -> int:
65
+ """Compose the optional local ReviewQueue over the spine."""
66
+ try:
67
+ from sqlalchemy import create_engine
68
+
69
+ from rememberstack.spine.review import ReviewQueue
70
+ from rememberstack.spine.settings import load_database_settings
71
+ except ModuleNotFoundError:
72
+ print(
73
+ "error: review commands require the 'rememberstack[server]' extra",
74
+ file=sys.stderr,
75
+ )
76
+ return 1
77
+
78
+ engine = create_engine(load_database_settings().sqlalchemy_url())
79
+ try:
80
+ queue = ReviewQueue(engine=engine)
81
+ if args.review_command == "list":
82
+ return _list(queue=queue, deployment_id=args.deployment)
83
+ return _decide(
84
+ queue=queue,
85
+ deployment_id=args.deployment,
86
+ review_id=args.review_id,
87
+ verdict=args.verdict,
88
+ reviewer=args.reviewer,
89
+ note=args.note,
90
+ )
91
+ finally:
92
+ engine.dispose()
93
+
94
+
95
+ def _run_budget(args: argparse.Namespace) -> int:
96
+ """Compose the local WorkLedger and print configured budget state."""
97
+ try:
98
+ from sqlalchemy import create_engine
99
+
100
+ from rememberstack.spine.settings import load_database_settings
101
+ from rememberstack.spine.work_ledger import WorkLedger
102
+ from rememberstack.spine.work_ledger import WorkLedgerSettings
103
+ except ModuleNotFoundError:
104
+ print(
105
+ "error: budget commands require the 'rememberstack[server]' extra",
106
+ file=sys.stderr,
107
+ )
108
+ return 1
109
+
110
+ engine = create_engine(load_database_settings().sqlalchemy_url())
111
+ try:
112
+ ledger = WorkLedger(engine=engine, settings=WorkLedgerSettings())
113
+ return _inspect_budgets(ledger=ledger, deployment_id=args.deployment)
114
+ finally:
115
+ engine.dispose()
116
+
117
+
118
+ def _run_ops(args: argparse.Namespace) -> int:
119
+ """Compose bounded local inspection, one-row replay, or an existing rebuild."""
120
+ try:
121
+ from rememberstack.model import ProcessingLane
122
+ from rememberstack.model import WorkLedgerError
123
+
124
+ operations_type = import_module(
125
+ "rememberstack.profiles.selfhost_operations"
126
+ ).SelfHostOperations
127
+ except ModuleNotFoundError:
128
+ print(
129
+ "error: ops commands require the 'rememberstack[server]' extra",
130
+ file=sys.stderr,
131
+ )
132
+ return 1
133
+
134
+ operations = operations_type.from_settings()
135
+ try:
136
+ if args.ops_command == "inspect":
137
+ report = operations.inspect(deployment_id=args.deployment)
138
+ print(report.model_dump_json())
139
+ return 0
140
+ if args.ops_command == "replay":
141
+ replayed = operations.replay(
142
+ deployment_id=args.deployment,
143
+ processing_id=args.processing_id,
144
+ attempt_allowance=args.attempts,
145
+ lane=None if args.lane is None else ProcessingLane(args.lane),
146
+ not_before=args.not_before,
147
+ )
148
+ print(replayed.model_dump_json())
149
+ return 0
150
+ result = operations.rebuild(
151
+ plane=args.plane,
152
+ deployment_id=args.deployment,
153
+ snapshot_root=args.snapshot_root,
154
+ workdir=args.workdir,
155
+ version=args.version,
156
+ )
157
+ print(json.dumps(result, default=str, sort_keys=True))
158
+ return 0
159
+ except (WorkLedgerError, ValueError, OSError) as error:
160
+ print(f"error: {error}", file=sys.stderr)
161
+ return 1
162
+ finally:
163
+ operations.close()
164
+
165
+
166
+ def _run_query(args: argparse.Namespace) -> int:
167
+ """Run a query command through the typed remote SDK."""
168
+ with MemoryClient.from_settings() as client:
169
+ if args.query_command == "list":
170
+ for descriptor in client.recipes():
171
+ print(descriptor.model_dump_json())
172
+ return 0
173
+ try:
174
+ arguments = dict(_split_arg(pair) for pair in args.arg)
175
+ except ValueError as error:
176
+ print(f"error: {error}", file=sys.stderr)
177
+ return 2
178
+ print(
179
+ client.run_recipe(name=args.recipe, arguments=arguments).model_dump_json()
180
+ )
181
+ return 0
182
+
183
+
184
+ def _run_ingest(args: argparse.Namespace) -> int:
185
+ """Push one local file to the deployment's E0 ingress."""
186
+ try:
187
+ with MemoryClient.from_settings() as client:
188
+ result = client.ingest(
189
+ args.file,
190
+ mime=args.mime,
191
+ title=args.title,
192
+ source_kind=args.source_kind,
193
+ source_ref=args.source_ref,
194
+ source_modified_at=args.source_modified_at,
195
+ versioning_mode=args.versioning_mode,
196
+ source_version_ref=args.source_version_ref,
197
+ )
198
+ except OSError as error:
199
+ print(f"error: could not read {args.file}: {error}", file=sys.stderr)
200
+ return 1
201
+ except ValueError as error:
202
+ print(f"error: {error}", file=sys.stderr)
203
+ return 2
204
+ print(result.model_dump_json())
205
+ return 0
206
+
207
+
208
+ def _run_connectors(args: argparse.Namespace) -> int:
209
+ """Manage connector configuration on the deployment API."""
210
+ with MemoryClient.from_settings() as client:
211
+ if args.connector_command == "list":
212
+ for connector in client.connectors():
213
+ print(connector.model_dump_json())
214
+ return 0
215
+ if args.connector_command == "add":
216
+ try:
217
+ configuration: dict[str, JsonValue] = dict(
218
+ _split_arg(pair) for pair in args.config
219
+ )
220
+ connector = ConnectorCreate(
221
+ kind=args.kind,
222
+ name=args.name,
223
+ configuration=configuration,
224
+ credential_ref=args.credential_ref,
225
+ )
226
+ except ValueError as error:
227
+ print(f"error: {error}", file=sys.stderr)
228
+ return 2
229
+ result = client.add_connector(connector=connector)
230
+ elif args.connector_command == "pause":
231
+ result = client.pause_connector(connector_id=args.connector_id)
232
+ else:
233
+ result = client.connector_status(connector_id=args.connector_id)
234
+ print(result.model_dump_json())
235
+ return 0
236
+
237
+
238
+ def _run_mcp() -> int:
239
+ """Expose the remote deployment recipe registry over MCP stdio."""
240
+ with MemoryClient.from_settings() as client:
241
+ return serve_mcp_stdio(server=RemoteRecipeMcpServer(client=client))
242
+
243
+
244
+ def query_list(*, client: httpx.Client) -> int:
245
+ """Print recipes from an injected client (the parity-testable CLI seam)."""
246
+ try:
247
+ for descriptor in MemoryClient(client=client).recipes():
248
+ print(descriptor.model_dump_json())
249
+ except MemoryApiError as error:
250
+ print(f"error: {error}", file=sys.stderr)
251
+ return 1
252
+ return 0
253
+
254
+
255
+ def query_run(*, client: httpx.Client, name: str, arg_pairs: list[str]) -> int:
256
+ """Run one recipe through an injected client and print its envelope."""
257
+ try:
258
+ arguments = dict(_split_arg(pair) for pair in arg_pairs)
259
+ except ValueError as error:
260
+ print(f"error: {error}", file=sys.stderr)
261
+ return 2
262
+ try:
263
+ envelope = MemoryClient(client=client).run_recipe(
264
+ name=name, arguments=arguments
265
+ )
266
+ except MemoryApiError as error:
267
+ print(f"error: {error}", file=sys.stderr)
268
+ return 1
269
+ print(envelope.model_dump_json())
270
+ return 0
271
+
272
+
273
+ def _split_arg(pair: str) -> tuple[str, str]:
274
+ """Split one ``key=value`` argument, or raise a clear error."""
275
+ key, separator, value = pair.partition("=")
276
+ if not separator or not key:
277
+ raise ValueError(f"argument {pair!r} is not key=value")
278
+ return key, value
279
+
280
+
281
+ def _list(*, queue: ReviewQueue, deployment_id: UUID) -> int:
282
+ """Print open items ranked by expected impact, one JSON line each."""
283
+ for item in queue.pending(deployment_id=deployment_id):
284
+ print(
285
+ json.dumps(
286
+ {
287
+ "review_id": str(item.review_id),
288
+ "kind": item.item_kind,
289
+ "expected_impact": item.expected_impact,
290
+ "blast_radius": item.blast_radius,
291
+ "status": item.status,
292
+ "candidate": item.candidate,
293
+ },
294
+ default=str,
295
+ )
296
+ )
297
+ return 0
298
+
299
+
300
+ def _inspect_budgets(*, ledger: WorkLedger, deployment_id: UUID) -> int:
301
+ """Print one current-window JSON record per configured deployment budget."""
302
+ for status in ledger.budget_status(deployment_id=deployment_id):
303
+ print(status.model_dump_json())
304
+ return 0
305
+
306
+
307
+ def _decide(
308
+ *,
309
+ queue: ReviewQueue,
310
+ deployment_id: UUID,
311
+ review_id: UUID,
312
+ verdict: str,
313
+ reviewer: str,
314
+ note: str | None,
315
+ ) -> int:
316
+ """Apply one verdict; the verdict picks the decision path by its name."""
317
+ try:
318
+ if verdict in _MERGE_VERDICTS:
319
+ events = queue.decide_merge(
320
+ deployment_id=deployment_id,
321
+ review_id=review_id,
322
+ verdict=verdict,
323
+ reviewer=reviewer,
324
+ note=note,
325
+ )
326
+ print(
327
+ json.dumps(
328
+ {"verdict": verdict, "merge_events": [str(e) for e in events]}
329
+ )
330
+ )
331
+ else:
332
+ queue.decide_support_withdrawn(
333
+ deployment_id=deployment_id,
334
+ review_id=review_id,
335
+ verdict=verdict,
336
+ reviewer=reviewer,
337
+ note=note,
338
+ )
339
+ print(json.dumps({"verdict": verdict}))
340
+ except ReviewDecisionError as error:
341
+ print(f"error: {error}", file=sys.stderr)
342
+ return 1
343
+ return 0
344
+
345
+
346
+ def _build_parser() -> argparse.ArgumentParser:
347
+ """Build the client-first command grammar."""
348
+ parser = argparse.ArgumentParser(
349
+ prog="remember", description="RememberStack command-line interface"
350
+ )
351
+ parser.add_argument(
352
+ "--version", action="version", version=f"RememberStack {__version__}"
353
+ )
354
+ commands = parser.add_subparsers(dest="command")
355
+
356
+ review = commands.add_parser("review", help="the D24 local review queue")
357
+ review_commands = review.add_subparsers(dest="review_command", required=True)
358
+ listing = review_commands.add_parser("list", help="open items, impact-ranked")
359
+ listing.add_argument("--deployment", type=UUID, required=True)
360
+ decide = review_commands.add_parser("decide", help="apply one verdict")
361
+ decide.add_argument("review_id", type=UUID)
362
+ decide.add_argument("--deployment", type=UUID, required=True)
363
+ decide.add_argument(
364
+ "--verdict", required=True, choices=(*_MERGE_VERDICTS, *_TRIAGE_VERDICTS)
365
+ )
366
+ decide.add_argument("--reviewer", required=True)
367
+ decide.add_argument("--note", default=None)
368
+
369
+ budget = commands.add_parser("budget", help="inspect configured spend ceilings")
370
+ budget_commands = budget.add_subparsers(dest="budget_command", required=True)
371
+ inspect = budget_commands.add_parser(
372
+ "inspect", help="current spend, tier attribution, and parked work"
373
+ )
374
+ inspect.add_argument("--deployment", type=UUID, required=True)
375
+
376
+ ops = commands.add_parser(
377
+ "ops", help="inspect durable state, replay one DLQ row, or rebuild"
378
+ )
379
+ ops_commands = ops.add_subparsers(dest="ops_command", required=True)
380
+ ops_inspect = ops_commands.add_parser(
381
+ "inspect", help="bounded pipeline, DLQ, projection, and currency report"
382
+ )
383
+ ops_inspect.add_argument("--deployment", type=UUID, required=True)
384
+ replay = ops_commands.add_parser("replay", help="reopen one dead-letter row")
385
+ replay.add_argument("processing_id", type=UUID)
386
+ replay.add_argument("--deployment", type=UUID, required=True)
387
+ replay.add_argument(
388
+ "--attempts",
389
+ type=int,
390
+ default=1,
391
+ help="additional handler attempts to grant (default: 1)",
392
+ )
393
+ replay.add_argument("--lane", choices=("steady", "backfill"))
394
+ replay.add_argument("--not-before", type=datetime.fromisoformat)
395
+ rebuild = ops_commands.add_parser(
396
+ "rebuild", help="invoke the existing P2 or P3 full-rebuild path"
397
+ )
398
+ rebuild.add_argument("--plane", choices=("p2", "p3"), required=True)
399
+ rebuild.add_argument("--deployment", type=UUID, required=True)
400
+ rebuild.add_argument("--snapshot-root", type=Path, required=True)
401
+ rebuild.add_argument("--workdir", type=Path, required=True)
402
+ rebuild.add_argument("--version", required=True)
403
+
404
+ query = commands.add_parser("query", help="query deployment recipes")
405
+ query_commands = query.add_subparsers(dest="query_command", required=True)
406
+ query_commands.add_parser("list", help="list the remote recipe tools")
407
+ run = query_commands.add_parser("run", help="run one recipe by name")
408
+ run.add_argument("recipe", help="the recipe name (see `remember query list`)")
409
+ run.add_argument(
410
+ "--arg", action="append", default=[], metavar="KEY=VALUE", help="repeatable"
411
+ )
412
+
413
+ ingest = commands.add_parser("ingest", help="push a file through E0")
414
+ ingest.add_argument("file", type=Path)
415
+ ingest.add_argument("--mime")
416
+ ingest.add_argument("--title")
417
+ ingest.add_argument("--source-kind")
418
+ ingest.add_argument("--source-ref")
419
+ ingest.add_argument("--source-modified-at", type=datetime.fromisoformat)
420
+ ingest.add_argument(
421
+ "--versioning-mode", choices=("snapshot", "living"), default="snapshot"
422
+ )
423
+ ingest.add_argument("--source-version-ref")
424
+
425
+ connectors = commands.add_parser(
426
+ "connectors", help="manage deployment-side connectors"
427
+ )
428
+ connector_commands = connectors.add_subparsers(
429
+ dest="connector_command", required=True
430
+ )
431
+ connector_commands.add_parser("list", help="list connectors")
432
+ add = connector_commands.add_parser("add", help="add connector configuration")
433
+ add.add_argument("kind")
434
+ add.add_argument("--name", required=True)
435
+ add.add_argument("--config", action="append", default=[], metavar="KEY=VALUE")
436
+ add.add_argument("--credential-ref")
437
+ pause = connector_commands.add_parser("pause", help="pause a connector")
438
+ pause.add_argument("connector_id", type=UUID)
439
+ status = connector_commands.add_parser("status", help="show connector status")
440
+ status.add_argument("connector_id", type=UUID)
441
+
442
+ commands.add_parser("mcp", help="serve remote recipes over MCP stdio")
443
+ return parser
444
+
445
+
446
+ if __name__ == "__main__": # pragma: no cover - exercised via the entry point
447
+ raise SystemExit(main())
@@ -0,0 +1,87 @@
1
+ """Deployment surface for rendering and publishing the D51 consumption skill."""
2
+
3
+ from pathlib import Path
4
+ from uuid import UUID
5
+ from uuid import uuid4
6
+
7
+ from rememberstack.core import render_consumption_skill
8
+ from rememberstack.model import ConsumptionRecipe
9
+ from rememberstack.model import ConsumptionSkillContext
10
+ from rememberstack.model import PublishedMounts
11
+ from rememberstack.model import Recipe
12
+ from rememberstack.model import RenderedConsumptionSkill
13
+ from rememberstack.spine.consumption import ConsumptionCatalog
14
+ from rememberstack.spine.recipes import RecipeRegistry
15
+
16
+
17
+ class ConsumptionSkillSurface:
18
+ """Render the skill from one deployment's live registries and mounts."""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ catalog: ConsumptionCatalog,
24
+ recipes: RecipeRegistry,
25
+ deployment_id: UUID,
26
+ ) -> None:
27
+ """Bind the renderer to one deployment and its two spine read models."""
28
+ self._catalog = catalog
29
+ self._recipes = recipes
30
+ self._deployment_id = deployment_id
31
+
32
+ @property
33
+ def deployment_id(self) -> UUID:
34
+ """The one deployment this skill surface renders."""
35
+ return self._deployment_id
36
+
37
+ def render(
38
+ self, *, mounts: PublishedMounts | None = None
39
+ ) -> RenderedConsumptionSkill:
40
+ """Render the current deployment-specific skill without writing it."""
41
+ if mounts is not None and mounts.deployment_id != self._deployment_id:
42
+ raise ValueError(
43
+ "the mount set and consumption skill serve different deployments"
44
+ )
45
+ recipes = tuple(
46
+ ConsumptionRecipe(
47
+ name=recipe.name,
48
+ description=recipe.description,
49
+ output_grain=recipe.output_grain,
50
+ answer_intent=recipe.answer_intent,
51
+ )
52
+ for recipe in _latest_recipes(
53
+ recipes=self._recipes.active(deployment_id=self._deployment_id)
54
+ )
55
+ )
56
+ return render_consumption_skill(
57
+ context=ConsumptionSkillContext(
58
+ deployment=self._catalog.deployment(deployment_id=self._deployment_id),
59
+ recipes=recipes,
60
+ mounts=mounts,
61
+ )
62
+ )
63
+
64
+ def publish(self, *, directory: Path, rendered: RenderedConsumptionSkill) -> Path:
65
+ """Atomically publish one already-rendered ``SKILL.md`` artifact."""
66
+ if rendered.deployment_id != self._deployment_id:
67
+ raise ValueError(
68
+ "the rendered skill and publisher serve different deployments"
69
+ )
70
+ directory.mkdir(parents=True, exist_ok=True)
71
+ destination = directory / rendered.filename
72
+ staging = directory / f".{rendered.filename}.{uuid4().hex}.tmp"
73
+ staging.write_text(data=rendered.content, encoding="utf-8")
74
+ staging.replace(target=destination)
75
+ return destination
76
+
77
+
78
+ def _latest_recipes(*, recipes: tuple[Recipe, ...]) -> tuple[Recipe, ...]:
79
+ """Keep one latest active recipe per name from registry-ordered rows."""
80
+ seen: set[str] = set()
81
+ latest: list[Recipe] = []
82
+ for recipe in recipes:
83
+ if recipe.name in seen:
84
+ continue
85
+ seen.add(recipe.name)
86
+ latest.append(recipe)
87
+ return tuple(latest)