memorykit 0.6.0__tar.gz → 0.7.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.
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.0 — 2026-08-29
4
+
5
+ - Make plugin-installed MCP safe and configurable in GitHub Copilot: declare
6
+ `stdio` explicitly and forward the operator-set
7
+ `CONTEXT_KIT_MEMORY_PROJECT` (or deprecated
8
+ `PRODUCTIVITY_SKILLS_MEMORY_PROJECT`) into the child process. The server still
9
+ refuses when no explicit scope is configured; prompt content cannot select
10
+ another project store.
11
+ - Move MCP proposal-only and absolute-source enforcement into the provider, so
12
+ quoted frontmatter follows the same parser as CLI capture and policy cannot
13
+ drift between surfaces. Provider validation now refuses a missing or
14
+ non-regular evidence source instead of skipping hash verification.
15
+ - Bound incoming JSON-RPC frames before decoding and reconcile the MCP server
16
+ version in packaging tests.
17
+ - Add MCP tool safety annotations and collapse missing-source versus
18
+ hash-mismatch failures into one capture refusal, avoiding a path-existence
19
+ distinction while preserving explicit failure.
20
+
3
21
  ## 0.6.0 — 2026-08-09
4
22
 
5
23
  - **Package the memory contract, validator, and MCP server as `memorykit`, ready
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: memorykit
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Provenance-bound durable memory: an immutable record contract, a validator, append-only review state, and a standard-library stdio MCP server. No Python dependencies; requires git on PATH.
5
5
  Project-URL: Homepage, https://github.com/mbeacom/context-kit
6
6
  Project-URL: Repository, https://github.com/mbeacom/context-kit
@@ -77,20 +77,16 @@ Installing `memory` also installs `context-handoff`, `verify`, and
77
77
  The contract, validator, and MCP server are also packaged as **`memorykit`**, a
78
78
  pure-standard-library Python package (ADR-0002, ADR-0009).
79
79
 
80
- > **Not on PyPI yet.** The package is publish-ready but the name has not been
81
- > claimed, so `pip install memorykit` does **not** currently install this — it
82
- > will fail, or, if someone else claims the name first, install something else
83
- > entirely. Until the release workflow runs, install from a clone.
84
-
85
80
  ```bash
86
- # From a clone of https://github.com/mbeacom/context-kit
87
- pip install ./plugins/memory # or: uv tool install ./plugins/memory
81
+ pip install memorykit # or: uv tool install memorykit
88
82
  ```
89
83
 
90
- Once published, that becomes:
84
+ To work from a clone instead — for contributing, or to run an unreleased
85
+ revision:
91
86
 
92
87
  ```bash
93
- pip install memorykit # or: uv tool install memorykit
88
+ # From a clone of https://github.com/mbeacom/context-kit
89
+ pip install ./plugins/memory # or: uv tool install ./plugins/memory
94
90
  ```
95
91
 
96
92
  Either way that is the whole install: no bootstrap step, no plugin runtime, no
@@ -130,7 +126,8 @@ is the reverse of the `indexkit` launcher's preference, and deliberate.
130
126
 
131
127
  ## Local-only reviewed records
132
128
 
133
- Python 3 is the only requirement. Configure an explicit project and plugin root:
129
+ Python 3 and Git on `PATH` are required. Configure an explicit project and
130
+ plugin root:
134
131
 
135
132
  ```bash
136
133
  export CONTEXT_KIT_MEMORY_PROJECT=owner/repository
@@ -223,12 +220,13 @@ authoring a `memory-v1` record from a candidate stays an explicit judgment step.
223
220
 
224
221
  See [`references/session-mining.md`](skills/memory-workflows/references/session-mining.md).
225
222
 
226
- ## MCP surface for non-plugin hosts
223
+ ## MCP surface
227
224
 
228
225
  An optional stdio MCP server exposes `memory_recall`, `memory_capture`, and
229
- `memory_review` so hosts that consume skills plus MCP can use durable memory
230
- without a plugin runtime. It is standard library only and shells out to the
231
- same provider, so the CLI and MCP paths cannot drift.
226
+ `memory_review` so hosts that consume skills plus MCP can use durable memory.
227
+ GitHub Copilot and Claude Code plugin installs discover it automatically;
228
+ package-only hosts can launch it directly. It is standard library only and
229
+ shells out to the same provider, so the CLI and MCP paths cannot drift.
232
230
 
233
231
  ```bash
234
232
  # From the plugin:
@@ -239,6 +237,14 @@ CONTEXT_KIT_MEMORY_PROJECT=owner/repository \
239
237
  CONTEXT_KIT_MEMORY_PROJECT=owner/repository memorykit-mcp
240
238
  ```
241
239
 
240
+ The bundled `.mcp.json` forwards `CONTEXT_KIT_MEMORY_PROJECT` from the host
241
+ environment into the plugin process. Set it to the current
242
+ `owner/repository` in the Copilot project environment or before launching the
243
+ host; do not edit Copilot's generated MCP entry. MCP capture records must use
244
+ an absolute `source` path because plugin hosts may launch the server outside
245
+ the active repository. The server refuses an unset project and never infers a
246
+ repository or falls back to a global store.
247
+
242
248
  The surface can propose memory but **cannot activate it**: a record whose
243
249
  frontmatter is not `review: proposed` is refused, and proposals stay out of
244
250
  active recall until promoted with the append-only `record-state` CLI.
@@ -270,7 +276,7 @@ explicit command there.
270
276
  | `/review-memory` | Review freshness, conflicts, and consolidation proposals. |
271
277
  | `/archive-handoff` | Explicitly preserve a validated handoff as historical memory. |
272
278
  | `memory-provider.py` | Launcher for the `memorykit` provider: stdlib validator, local store, MemPalace adapter, and hook dispatcher. |
273
- | `src/memorykit/` | The packaged engine (`memorykit`, not yet on PyPI): contract, validator, provider, MCP server. |
279
+ | `src/memorykit/` | The packaged `memorykit` engine: contract, validator, provider, MCP server. |
274
280
 
275
281
  ## Safety boundaries
276
282
 
@@ -34,20 +34,16 @@ Installing `memory` also installs `context-handoff`, `verify`, and
34
34
  The contract, validator, and MCP server are also packaged as **`memorykit`**, a
35
35
  pure-standard-library Python package (ADR-0002, ADR-0009).
36
36
 
37
- > **Not on PyPI yet.** The package is publish-ready but the name has not been
38
- > claimed, so `pip install memorykit` does **not** currently install this — it
39
- > will fail, or, if someone else claims the name first, install something else
40
- > entirely. Until the release workflow runs, install from a clone.
41
-
42
37
  ```bash
43
- # From a clone of https://github.com/mbeacom/context-kit
44
- pip install ./plugins/memory # or: uv tool install ./plugins/memory
38
+ pip install memorykit # or: uv tool install memorykit
45
39
  ```
46
40
 
47
- Once published, that becomes:
41
+ To work from a clone instead — for contributing, or to run an unreleased
42
+ revision:
48
43
 
49
44
  ```bash
50
- pip install memorykit # or: uv tool install memorykit
45
+ # From a clone of https://github.com/mbeacom/context-kit
46
+ pip install ./plugins/memory # or: uv tool install ./plugins/memory
51
47
  ```
52
48
 
53
49
  Either way that is the whole install: no bootstrap step, no plugin runtime, no
@@ -87,7 +83,8 @@ is the reverse of the `indexkit` launcher's preference, and deliberate.
87
83
 
88
84
  ## Local-only reviewed records
89
85
 
90
- Python 3 is the only requirement. Configure an explicit project and plugin root:
86
+ Python 3 and Git on `PATH` are required. Configure an explicit project and
87
+ plugin root:
91
88
 
92
89
  ```bash
93
90
  export CONTEXT_KIT_MEMORY_PROJECT=owner/repository
@@ -180,12 +177,13 @@ authoring a `memory-v1` record from a candidate stays an explicit judgment step.
180
177
 
181
178
  See [`references/session-mining.md`](skills/memory-workflows/references/session-mining.md).
182
179
 
183
- ## MCP surface for non-plugin hosts
180
+ ## MCP surface
184
181
 
185
182
  An optional stdio MCP server exposes `memory_recall`, `memory_capture`, and
186
- `memory_review` so hosts that consume skills plus MCP can use durable memory
187
- without a plugin runtime. It is standard library only and shells out to the
188
- same provider, so the CLI and MCP paths cannot drift.
183
+ `memory_review` so hosts that consume skills plus MCP can use durable memory.
184
+ GitHub Copilot and Claude Code plugin installs discover it automatically;
185
+ package-only hosts can launch it directly. It is standard library only and
186
+ shells out to the same provider, so the CLI and MCP paths cannot drift.
189
187
 
190
188
  ```bash
191
189
  # From the plugin:
@@ -196,6 +194,14 @@ CONTEXT_KIT_MEMORY_PROJECT=owner/repository \
196
194
  CONTEXT_KIT_MEMORY_PROJECT=owner/repository memorykit-mcp
197
195
  ```
198
196
 
197
+ The bundled `.mcp.json` forwards `CONTEXT_KIT_MEMORY_PROJECT` from the host
198
+ environment into the plugin process. Set it to the current
199
+ `owner/repository` in the Copilot project environment or before launching the
200
+ host; do not edit Copilot's generated MCP entry. MCP capture records must use
201
+ an absolute `source` path because plugin hosts may launch the server outside
202
+ the active repository. The server refuses an unset project and never infers a
203
+ repository or falls back to a global store.
204
+
199
205
  The surface can propose memory but **cannot activate it**: a record whose
200
206
  frontmatter is not `review: proposed` is refused, and proposals stay out of
201
207
  active recall until promoted with the append-only `record-state` CLI.
@@ -227,7 +233,7 @@ explicit command there.
227
233
  | `/review-memory` | Review freshness, conflicts, and consolidation proposals. |
228
234
  | `/archive-handoff` | Explicitly preserve a validated handoff as historical memory. |
229
235
  | `memory-provider.py` | Launcher for the `memorykit` provider: stdlib validator, local store, MemPalace adapter, and hook dispatcher. |
230
- | `src/memorykit/` | The packaged engine (`memorykit`, not yet on PyPI): contract, validator, provider, MCP server. |
236
+ | `src/memorykit/` | The packaged `memorykit` engine: contract, validator, provider, MCP server. |
231
237
 
232
238
  ## Safety boundaries
233
239
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "memorykit"
3
- version = "0.6.0"
3
+ version = "0.7.0"
4
4
  description = "Provenance-bound durable memory: an immutable record contract, a validator, append-only review state, and a standard-library stdio MCP server. No Python dependencies; requires git on PATH."
5
5
  requires-python = ">=3.10"
6
6
  keywords = [
@@ -19,6 +19,6 @@ Two entry points, one implementation:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- __version__ = "0.6.0"
22
+ __version__ = "0.7.0"
23
23
 
24
24
  __all__ = ["__version__"]
@@ -26,7 +26,7 @@ from pathlib import Path
26
26
  from typing import Any
27
27
 
28
28
  SERVER_NAME = "context-kit-memory"
29
- SERVER_VERSION = "0.3.0"
29
+ SERVER_VERSION = "0.7.0"
30
30
  # Newest first. The client's requested version is echoed when supported.
31
31
  SUPPORTED_PROTOCOLS = ("2025-06-18", "2025-03-26", "2024-11-05")
32
32
  # The provider is this package's sibling module, so one path is correct in both
@@ -38,6 +38,11 @@ SUPPORTED_PROTOCOLS = ("2025-06-18", "2025-03-26", "2024-11-05")
38
38
  PROVIDER = Path(__file__).resolve().with_name("provider.py")
39
39
  CALL_TIMEOUT_SECONDS = 120.0
40
40
  MAX_RECORD_BYTES = 32 * 1024
41
+ MAX_FRAME_CHARS = MAX_RECORD_BYTES * 8
42
+ EVIDENCE_ERROR_MARKERS = (
43
+ "referenced source is not a readable file",
44
+ "source_hash does not match the referenced source file",
45
+ )
41
46
 
42
47
  PARSE_ERROR = -32700
43
48
  INVALID_REQUEST = -32600
@@ -70,14 +75,15 @@ TOOLS: list[dict[str, Any]] = [
70
75
  "required": ["query"],
71
76
  "additionalProperties": False,
72
77
  },
78
+ "annotations": {"readOnlyHint": True, "openWorldHint": False},
73
79
  },
74
80
  {
75
81
  "name": "memory_capture",
76
82
  "description": (
77
83
  "Persist a context-kit/memory-v1 record as review: proposed. The "
78
- "record must cite real evidence: its source_hash is verified "
79
- "against the source file. Proposed records are inert until a human "
80
- "accepts them with the record-state CLI."
84
+ "record must cite real evidence at an absolute source path: its "
85
+ "source_hash is verified against the source file. Proposed records "
86
+ "are inert until a human accepts them with the record-state CLI."
81
87
  ),
82
88
  "inputSchema": {
83
89
  "type": "object",
@@ -94,6 +100,12 @@ TOOLS: list[dict[str, Any]] = [
94
100
  "required": ["record"],
95
101
  "additionalProperties": False,
96
102
  },
103
+ "annotations": {
104
+ "readOnlyHint": False,
105
+ "destructiveHint": False,
106
+ "idempotentHint": True,
107
+ "openWorldHint": False,
108
+ },
97
109
  },
98
110
  {
99
111
  "name": "memory_review",
@@ -106,6 +118,7 @@ TOOLS: list[dict[str, Any]] = [
106
118
  "properties": {},
107
119
  "additionalProperties": False,
108
120
  },
121
+ "annotations": {"readOnlyHint": True, "openWorldHint": False},
109
122
  },
110
123
  ]
111
124
 
@@ -119,26 +132,10 @@ def _log(message: str) -> None:
119
132
  print(f"{SERVER_NAME}: {message}", file=sys.stderr, flush=True)
120
133
 
121
134
 
122
- def _project_scope() -> str:
123
- for name in ("CONTEXT_KIT_MEMORY_PROJECT", "CLAUDE_PLUGIN_OPTION_PROJECT"):
124
- # Portable first, then the Claude userConfig fallback the CLI accepts.
125
- # Checking only the portable name would make every tool refuse on a
126
- # normal Claude install configured through the plugin's option.
127
- project = os.environ.get(name, "").strip()
128
- if project:
129
- return project
130
- raise ToolError(
131
- "no memory project is configured; set CONTEXT_KIT_MEMORY_PROJECT to "
132
- "an explicit owner/repository. Memory is never read from or written "
133
- "to an inferred global store."
134
- )
135
-
136
-
137
- def _run_provider(argv: list[str]) -> str:
135
+ def _run_provider(argv: list[str], *, redact_evidence_errors: bool = False) -> str:
138
136
  if not PROVIDER.is_file():
139
137
  raise ToolError(f"memory provider script is missing: {PROVIDER}")
140
- project = _project_scope()
141
- command = [sys.executable, str(PROVIDER), *argv, "--project", project]
138
+ command = [sys.executable, str(PROVIDER), *argv]
142
139
  try:
143
140
  result = subprocess.run(
144
141
  command,
@@ -154,23 +151,14 @@ def _run_provider(argv: list[str]) -> str:
154
151
  raise ToolError(f"memory command could not run: {exc}") from exc
155
152
  if result.returncode != 0:
156
153
  detail = result.stderr.decode("utf-8", errors="replace").strip()
154
+ if redact_evidence_errors and any(
155
+ marker in detail for marker in EVIDENCE_ERROR_MARKERS
156
+ ):
157
+ detail = "memory capture refused: cited source evidence did not validate"
157
158
  raise ToolError(detail or f"memory command exited {result.returncode}")
158
159
  return result.stdout.decode("utf-8", errors="replace").strip()
159
160
 
160
161
 
161
- def _frontmatter_value(record: str, field: str) -> str | None:
162
- lines = record.splitlines()
163
- if not lines or lines[0].strip() != "---":
164
- return None
165
- for line in lines[1:]:
166
- if line.strip() == "---":
167
- break
168
- key, separator, value = line.partition(":")
169
- if separator and key.strip() == field:
170
- return value.strip()
171
- return None
172
-
173
-
174
162
  def _tool_memory_recall(arguments: dict[str, Any]) -> str:
175
163
  query = arguments.get("query")
176
164
  if not isinstance(query, str) or not query.strip():
@@ -192,38 +180,21 @@ def _tool_memory_capture(arguments: dict[str, Any]) -> str:
192
180
  raise ToolError(
193
181
  f"record exceeds {MAX_RECORD_BYTES} bytes; keep a memory atomic"
194
182
  )
195
- review = _frontmatter_value(record, "review")
196
- if review is None:
197
- raise ToolError("record is missing flat YAML frontmatter with a `review` field")
198
- if review != "proposed":
199
- # `capture` takes the initial state from frontmatter, so without this
200
- # guard an agent could write `review: accepted` and activate a memory
201
- # with no human review at all.
202
- raise ToolError(
203
- "this surface can only propose memory, but the record declares "
204
- f"review: {review}. Set `review: proposed` and promote it later "
205
- "with `memory-provider.py record-state <id> --review accepted "
206
- "--reason ...` after the evidence has been checked."
207
- )
208
- # `validate_memory` verifies `source_hash` only when the source is a
209
- # *regular file* (`source.is_file()`), so `exists()` here would be a weaker
210
- # gate than the one it exists to mirror: a record citing a directory would
211
- # pass this check, skip hash verification entirely, and persist with any
212
- # 64-character hash and unverifiable provenance. Match the provider.
213
- source = _frontmatter_value(record, "source")
214
- if not source:
215
- raise ToolError("record is missing a `source` field citing its evidence")
216
- if not Path(source).expanduser().is_file():
217
- raise ToolError(
218
- f"the cited source is not a readable file: {source}. A memory must "
219
- "point at evidence that can be re-read and hashed."
220
- )
221
183
  handle, temporary = tempfile.mkstemp(prefix="memory-capture-", suffix=".md")
222
184
  path = Path(temporary)
223
185
  try:
224
186
  with os.fdopen(handle, "wb") as stream:
225
187
  stream.write(raw)
226
- return _run_provider(["capture", str(path)])
188
+ return _run_provider(
189
+ [
190
+ "capture",
191
+ str(path),
192
+ "--require-review",
193
+ "proposed",
194
+ "--require-absolute-source",
195
+ ],
196
+ redact_evidence_errors=True,
197
+ )
227
198
  finally:
228
199
  path.unlink(missing_ok=True)
229
200
 
@@ -326,7 +297,22 @@ def _write(sink: Any, payload: dict[str, Any]) -> None:
326
297
  def serve(stdin: Any = None, stdout: Any = None) -> int:
327
298
  source = stdin if stdin is not None else sys.stdin
328
299
  sink = stdout if stdout is not None else sys.stdout
329
- for line in source:
300
+ while True:
301
+ line = source.readline(MAX_FRAME_CHARS + 1)
302
+ if not line:
303
+ break
304
+ if len(line) > MAX_FRAME_CHARS:
305
+ while line and not line.endswith("\n"):
306
+ line = source.readline(MAX_FRAME_CHARS + 1)
307
+ _write(
308
+ sink,
309
+ _error(
310
+ None,
311
+ INVALID_REQUEST,
312
+ f"JSON-RPC frame exceeds {MAX_FRAME_CHARS} characters",
313
+ ),
314
+ )
315
+ continue
330
316
  line = line.strip()
331
317
  if not line:
332
318
  continue
@@ -442,6 +442,7 @@ def _config(args: argparse.Namespace) -> Config:
442
442
  getattr(args, "provider", None)
443
443
  or _first_env(
444
444
  "CONTEXT_KIT_MEMORY_PROVIDER",
445
+ "PRODUCTIVITY_SKILLS_MEMORY_PROVIDER",
445
446
  "CLAUDE_PLUGIN_OPTION_PROVIDER",
446
447
  )
447
448
  or "none"
@@ -453,6 +454,7 @@ def _config(args: argparse.Namespace) -> Config:
453
454
  getattr(args, "home", None)
454
455
  or _first_env(
455
456
  "CONTEXT_KIT_MEMORY_HOME",
457
+ "PRODUCTIVITY_SKILLS_MEMORY_HOME",
456
458
  "CLAUDE_PLUGIN_OPTION_MEMORY_HOME",
457
459
  )
458
460
  or "~/.local/share/context-kit/memory"
@@ -460,14 +462,17 @@ def _config(args: argparse.Namespace) -> Config:
460
462
  home = Path(home_value).expanduser().resolve()
461
463
  project = getattr(args, "project", None) or _first_env(
462
464
  "CONTEXT_KIT_MEMORY_PROJECT",
465
+ "PRODUCTIVITY_SKILLS_MEMORY_PROJECT",
463
466
  "CLAUDE_PLUGIN_OPTION_PROJECT",
464
467
  )
465
468
  auto_value = _first_env(
466
469
  "CONTEXT_KIT_MEMORY_AUTO_CAPTURE",
470
+ "PRODUCTIVITY_SKILLS_MEMORY_AUTO_CAPTURE",
467
471
  "CLAUDE_PLUGIN_OPTION_AUTO_CAPTURE",
468
472
  )
469
473
  recall_value = _first_env(
470
474
  "CONTEXT_KIT_MEMORY_RECALL_ON_START",
475
+ "PRODUCTIVITY_SKILLS_MEMORY_RECALL_ON_START",
471
476
  "CLAUDE_PLUGIN_OPTION_RECALL_ON_START",
472
477
  )
473
478
  return Config(
@@ -611,7 +616,13 @@ def _read_bounded(
611
616
  return raw, text
612
617
 
613
618
 
614
- def validate_memory(path: Path, *, verify_source: bool = True) -> dict[str, object]:
619
+ def validate_memory(
620
+ path: Path,
621
+ *,
622
+ verify_source: bool = True,
623
+ require_review: str | None = None,
624
+ require_absolute_source: bool = False,
625
+ ) -> dict[str, object]:
615
626
  raw, text = _read_bounded(path)
616
627
  fields, body = _parse_frontmatter(text)
617
628
  missing = [field for field in REQUIRED_FIELDS if field not in fields]
@@ -633,6 +644,11 @@ def validate_memory(path: Path, *, verify_source: bool = True) -> dict[str, obje
633
644
  raise Refusal(f"freshness must be one of {sorted(FRESHNESS_STATES)}")
634
645
  if fields["review"] not in REVIEW_STATES:
635
646
  raise Refusal(f"review must be one of {sorted(REVIEW_STATES)}")
647
+ if require_review is not None and fields["review"] != require_review:
648
+ raise Refusal(
649
+ f"memory review must be {require_review!r} for this operation; "
650
+ f"got {fields['review']!r}"
651
+ )
636
652
  if not HASH_RE.fullmatch(fields["source_hash"]):
637
653
  raise Refusal("source_hash must be a lowercase SHA-256 digest")
638
654
  _validate_timestamp(fields["observed_at"], "observed_at")
@@ -666,8 +682,18 @@ def validate_memory(path: Path, *, verify_source: bool = True) -> dict[str, obje
666
682
  _nonempty_section(sections["## Review Notes"], "Review Notes")
667
683
 
668
684
  source = Path(fields["source"]).expanduser()
669
- if verify_source and source.is_file():
670
- actual = hashlib.sha256(source.read_bytes()).hexdigest()
685
+ if require_absolute_source and not source.is_absolute():
686
+ raise Refusal("memory source must be an absolute path for this operation")
687
+ if verify_source:
688
+ if not source.is_file():
689
+ raise Refusal(f"referenced source is not a readable file: {source}")
690
+ try:
691
+ source_bytes = source.read_bytes()
692
+ except OSError as exc:
693
+ raise Refusal(
694
+ f"referenced source is not a readable file: {source}"
695
+ ) from exc
696
+ actual = hashlib.sha256(source_bytes).hexdigest()
671
697
  if actual != fields["source_hash"]:
672
698
  raise Refusal("source_hash does not match the referenced source file")
673
699
  return {
@@ -1407,7 +1433,11 @@ def _provider_version(config: Config) -> tuple[str, str]:
1407
1433
 
1408
1434
  def _capture_memory(args: argparse.Namespace, config: Config) -> int:
1409
1435
  source = Path(args.artifact).expanduser().resolve()
1410
- metadata = validate_memory(source)
1436
+ metadata = validate_memory(
1437
+ source,
1438
+ require_review=args.require_review,
1439
+ require_absolute_source=args.require_absolute_source,
1440
+ )
1411
1441
  _assert_project_matches(metadata, config)
1412
1442
  raw = source.read_bytes()
1413
1443
  destination = config.records_path / f"{metadata['id']}.md"
@@ -2761,6 +2791,8 @@ def _parser() -> argparse.ArgumentParser:
2761
2791
  capture = sub.add_parser("capture")
2762
2792
  capture.add_argument("artifact")
2763
2793
  capture.add_argument("--local-only", action="store_true")
2794
+ capture.add_argument("--require-review", choices=sorted(REVIEW_STATES))
2795
+ capture.add_argument("--require-absolute-source", action="store_true")
2764
2796
  _add_config_args(capture)
2765
2797
 
2766
2798
  archive = sub.add_parser("archive-handoff")
File without changes
File without changes