delimit-cli 4.3.4 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +25 -18
  3. package/adapters/codex-security.js +64 -0
  4. package/adapters/codex-skill.js +78 -0
  5. package/adapters/cursor-rules.js +73 -0
  6. package/bin/delimit-setup.js +23 -0
  7. package/gateway/ai/backends/governance_bridge.py +168 -2
  8. package/gateway/ai/backends/memory_bridge.py +218 -3
  9. package/gateway/ai/backends/tools_design.py +563 -83
  10. package/gateway/ai/backends/tools_infra.py +21 -7
  11. package/gateway/ai/backends/tools_real.py +3 -1
  12. package/gateway/ai/content_grounding/__init__.py +98 -0
  13. package/gateway/ai/content_grounding/build.py +350 -0
  14. package/gateway/ai/content_grounding/consume.py +280 -0
  15. package/gateway/ai/content_grounding/features.py +218 -0
  16. package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +9 -0
  17. package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +9 -0
  18. package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +17 -0
  19. package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +17 -0
  20. package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +17 -0
  21. package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +18 -0
  22. package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +18 -0
  23. package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +23 -0
  24. package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +16 -0
  25. package/gateway/ai/content_grounding/schemas/claim.schema.json +40 -0
  26. package/gateway/ai/content_grounding/schemas/event.schema.json +23 -0
  27. package/gateway/ai/content_grounding/schemas.py +276 -0
  28. package/gateway/ai/content_grounding/telemetry.py +221 -0
  29. package/gateway/ai/governance.py +89 -0
  30. package/gateway/ai/hot_reload.py +148 -7
  31. package/gateway/ai/inbox_drafts/__init__.py +61 -0
  32. package/gateway/ai/inbox_drafts/registry.py +412 -0
  33. package/gateway/ai/inbox_drafts/schema.py +374 -0
  34. package/gateway/ai/inbox_executor.py +565 -0
  35. package/gateway/ai/ledger_manager.py +1483 -25
  36. package/gateway/ai/license_core.py +3 -1
  37. package/gateway/ai/mcp_bridge.py +1 -1
  38. package/gateway/ai/reddit_proxy.py +8 -6
  39. package/gateway/ai/server.py +451 -9
  40. package/gateway/ai/supabase_sync.py +47 -7
  41. package/gateway/ai/swarm.py +1 -1
  42. package/gateway/ai/workers/executor.py +1 -1
  43. package/gateway/core/diff_engine_v2.py +45 -10
  44. package/gateway/core/zero_spec/express_extractor.py +1 -1
  45. package/lib/delimit-template.js +5 -0
  46. package/package.json +1 -1
@@ -19,8 +19,28 @@ def _ensure_dir():
19
19
  MEMORY_DIR.mkdir(parents=True, exist_ok=True)
20
20
 
21
21
 
22
- def store(content: str, tags: Optional[list] = None, context: Optional[str] = None) -> Dict[str, Any]:
23
- """Store a memory entry."""
22
+ def store(
23
+ content: str,
24
+ tags: Optional[list] = None,
25
+ context: Optional[str] = None,
26
+ hot_load: bool = False,
27
+ ) -> Dict[str, Any]:
28
+ """Store a memory entry.
29
+
30
+ LED-1165 Phase 2 #5 PR-A: opt-in `hot_load` flag marks an entry for
31
+ one-way projection into the Claude Code auto-memory `MEMORY.md` file
32
+ (managed-section). The projection writer is shipped in PR-B; this PR
33
+ only persists the flag.
34
+
35
+ Args:
36
+ content: The content to remember.
37
+ tags: Optional categorization tags.
38
+ context: Optional context about when/why this was stored.
39
+ hot_load: When True, mark the entry for projection into the
40
+ Claude Code MEMORY.md hot-load index. Default False — entries
41
+ are durable in delimit_memory but not projected. Existing
42
+ entries are unaffected (treated as hot_load=False).
43
+ """
24
44
  _ensure_dir()
25
45
 
26
46
  # Generate ID from content hash
@@ -33,12 +53,18 @@ def store(content: str, tags: Optional[list] = None, context: Optional[str] = No
33
53
  "tags": tags or [],
34
54
  "context": context or "",
35
55
  "created_at": ts,
56
+ "hot_load": bool(hot_load),
36
57
  }
37
58
 
38
59
  path = MEMORY_DIR / f"{mem_id}.json"
39
60
  path.write_text(json.dumps(entry, indent=2))
40
61
 
41
- return {"stored": mem_id, "path": str(path), "created_at": ts}
62
+ return {
63
+ "stored": mem_id,
64
+ "path": str(path),
65
+ "created_at": ts,
66
+ "hot_load": bool(hot_load),
67
+ }
42
68
 
43
69
 
44
70
  def search(query: str, limit: int = 10) -> Dict[str, Any]:
@@ -88,8 +114,197 @@ def get_recent(limit: int = 5) -> Dict[str, Any]:
88
114
  "content": entry.get("content", "")[:500],
89
115
  "tags": entry.get("tags", []),
90
116
  "created_at": entry.get("created_at", ""),
117
+ "hot_load": bool(entry.get("hot_load", False)),
118
+ })
119
+ except Exception:
120
+ pass
121
+
122
+ return {"results": entries, "count": len(entries)}
123
+
124
+
125
+ def list_hot(limit: int = 200) -> Dict[str, Any]:
126
+ """Return all entries marked hot_load=True, newest first.
127
+
128
+ LED-1165 Phase 2 #5 PR-A: backing query for the MEMORY.md projection
129
+ writer that PR-B will introduce. Returned entries are full content
130
+ (not truncated) so the projection writer can render them faithfully.
131
+
132
+ Args:
133
+ limit: cap on entries returned. Default 200; the projection
134
+ writer hard-caps the rendered MEMORY.md size so this is
135
+ mostly belt-and-braces.
136
+
137
+ Returns:
138
+ {
139
+ "results": [{id, content, tags, context, created_at, hot_load}, ...],
140
+ "count": int,
141
+ }
142
+ """
143
+ _ensure_dir()
144
+ entries = []
145
+
146
+ for f in sorted(MEMORY_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
147
+ if len(entries) >= limit:
148
+ break
149
+ try:
150
+ entry = json.loads(f.read_text())
151
+ if not entry.get("hot_load"):
152
+ continue
153
+ entries.append({
154
+ "id": entry.get("id", f.stem),
155
+ "content": entry.get("content", ""),
156
+ "tags": entry.get("tags", []),
157
+ "context": entry.get("context", ""),
158
+ "created_at": entry.get("created_at", ""),
159
+ "hot_load": True,
91
160
  })
92
161
  except Exception:
93
162
  pass
94
163
 
95
164
  return {"results": entries, "count": len(entries)}
165
+
166
+
167
+ # ── LED-1165 Phase 2 #5 PR-B: MEMORY.md projection writer ──────────────
168
+
169
+ # Default target — Claude Code's auto-memory MEMORY.md for the current
170
+ # project (this is where Claude Code reads memory entries on session
171
+ # start). Override via env or function arg for testing / non-default
172
+ # project layouts.
173
+ DEFAULT_MEMORY_MD = Path.home() / ".claude" / "projects" / "-root" / "memory" / "MEMORY.md"
174
+
175
+ PROJECTION_START_MARKER = "<!-- delimit:start -->"
176
+ PROJECTION_END_MARKER = "<!-- delimit:end -->"
177
+
178
+
179
+ def _format_hot_entry_as_markdown(entry: Dict[str, Any]) -> str:
180
+ """Render one delimit_memory entry as a Markdown bullet for the
181
+ MEMORY.md hot-load index. Intentionally compact — the index loads
182
+ into every Claude Code session, so each entry should fit on a
183
+ line or two."""
184
+ mid = entry.get("id", "?")
185
+ content = (entry.get("content") or "").strip()
186
+ # Single-line the content but cap at ~280 chars so the line stays
187
+ # readable. Long content stays in delimit_memory; index is just
188
+ # the hook for Claude to know it exists.
189
+ one_line = " ".join(content.split())
190
+ if len(one_line) > 280:
191
+ one_line = one_line[:277] + "..."
192
+ tags = entry.get("tags") or []
193
+ tag_str = (" [tags: " + ", ".join(tags) + "]") if tags else ""
194
+ ctx = (entry.get("context") or "").strip()
195
+ ctx_line = f"\n > {ctx[:200]}" if ctx else ""
196
+ return f"- **{mid}**{tag_str} — {one_line}{ctx_line}"
197
+
198
+
199
+ def _render_managed_block(entries: List[Dict[str, Any]]) -> str:
200
+ """Render the full managed section (between markers) for the
201
+ MEMORY.md hot-load index. Includes a one-line preamble explaining
202
+ the section so anyone editing the file understands what it is."""
203
+ if not entries:
204
+ body = (
205
+ "_No hot-load memory entries. Add one with "
206
+ "`delimit_memory_store(content=\"...\", hot_load=True)`._\n"
207
+ )
208
+ else:
209
+ bullets = "\n".join(_format_hot_entry_as_markdown(e) for e in entries)
210
+ body = bullets + "\n"
211
+
212
+ # Brief header + body + caveat
213
+ header = (
214
+ "## Delimit hot memory (auto-projected from delimit_memory)\n"
215
+ "\n"
216
+ f"_Auto-managed by `delimit_memory_index` — projection of {len(entries)} "
217
+ "entry/entries flagged `hot_load=True`. Edits inside this block are "
218
+ "overwritten on next projection. Add an entry via "
219
+ "`delimit_memory_store(content=\"...\", hot_load=True)`._\n"
220
+ "\n"
221
+ )
222
+ return PROJECTION_START_MARKER + "\n" + header + body + PROJECTION_END_MARKER
223
+
224
+
225
+ def project_to_memory_md(
226
+ target_path: Optional[Path] = None,
227
+ dry_run: bool = False,
228
+ limit: int = 200,
229
+ ) -> Dict[str, Any]:
230
+ """One-way projection: render hot_load=True entries from delimit_memory
231
+ into a managed section of Claude Code's MEMORY.md.
232
+
233
+ LED-1165 Phase 2 #5 PR-B. Composes PR-A's `list_hot` helper with a
234
+ managed-section markdown writer. NEVER reads MEMORY.md back into
235
+ delimit_memory — that's the explicit deliberation rule (Anthropic
236
+ owns auto-memory's format; we don't risk drift).
237
+
238
+ Behavior:
239
+ - If target_path's file does NOT exist, create it with just the
240
+ managed section (no other content).
241
+ - If target_path's file exists and contains markers, replace the
242
+ section between them. Content outside markers is preserved.
243
+ - If target_path's file exists but has no markers, APPEND a new
244
+ managed section to the end. Does NOT touch existing content.
245
+
246
+ Args:
247
+ target_path: where to write. Default DEFAULT_MEMORY_MD.
248
+ dry_run: True returns the rendered content without writing.
249
+ limit: cap on entries projected. Default 200 (matches list_hot).
250
+
251
+ Returns:
252
+ {
253
+ "target": str,
254
+ "dry_run": bool,
255
+ "entries": int,
256
+ "wrote_chars": int (or "would_write_chars"),
257
+ "had_existing_block": bool,
258
+ "had_existing_file": bool,
259
+ "preserved_user_content": bool,
260
+ }
261
+ """
262
+ if target_path is None:
263
+ target_path = DEFAULT_MEMORY_MD
264
+
265
+ target_path = Path(target_path)
266
+ hot = list_hot(limit=limit)
267
+ entries = hot.get("results", [])
268
+
269
+ block = _render_managed_block(entries)
270
+
271
+ had_existing_file = target_path.exists()
272
+ had_existing_block = False
273
+ preserved_user_content = False
274
+
275
+ if had_existing_file:
276
+ existing = target_path.read_text()
277
+ start_idx = existing.find(PROJECTION_START_MARKER)
278
+ end_idx = existing.find(PROJECTION_END_MARKER)
279
+ if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
280
+ had_existing_block = True
281
+ preserved_user_content = bool(
282
+ existing[:start_idx].strip() or existing[end_idx + len(PROJECTION_END_MARKER):].strip()
283
+ )
284
+ new_content = (
285
+ existing[:start_idx]
286
+ + block
287
+ + existing[end_idx + len(PROJECTION_END_MARKER):]
288
+ )
289
+ else:
290
+ # No markers — append to end, preserving everything above
291
+ preserved_user_content = bool(existing.strip())
292
+ sep = "" if existing.endswith("\n\n") else ("\n" if existing.endswith("\n") else "\n\n")
293
+ new_content = existing + sep + block + "\n"
294
+ else:
295
+ # Brand new file — just the managed section
296
+ new_content = block + "\n"
297
+
298
+ if not dry_run:
299
+ target_path.parent.mkdir(parents=True, exist_ok=True)
300
+ target_path.write_text(new_content)
301
+
302
+ return {
303
+ "target": str(target_path),
304
+ "dry_run": dry_run,
305
+ "entries": len(entries),
306
+ "wrote_chars" if not dry_run else "would_write_chars": len(new_content),
307
+ "had_existing_block": had_existing_block,
308
+ "had_existing_file": had_existing_file,
309
+ "preserved_user_content": preserved_user_content,
310
+ }