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.
- package/CHANGELOG.md +96 -0
- package/README.md +25 -18
- package/adapters/codex-security.js +64 -0
- package/adapters/codex-skill.js +78 -0
- package/adapters/cursor-rules.js +73 -0
- package/bin/delimit-setup.js +23 -0
- package/gateway/ai/backends/governance_bridge.py +168 -2
- package/gateway/ai/backends/memory_bridge.py +218 -3
- package/gateway/ai/backends/tools_design.py +563 -83
- package/gateway/ai/backends/tools_infra.py +21 -7
- package/gateway/ai/backends/tools_real.py +3 -1
- package/gateway/ai/content_grounding/__init__.py +98 -0
- package/gateway/ai/content_grounding/build.py +350 -0
- package/gateway/ai/content_grounding/consume.py +280 -0
- package/gateway/ai/content_grounding/features.py +218 -0
- package/gateway/ai/content_grounding/fixtures/fail/01_missing_evidence.json +9 -0
- package/gateway/ai/content_grounding/fixtures/fail/02_unknown_evidence_prefix.json +9 -0
- package/gateway/ai/content_grounding/fixtures/fail/03_banned_comparative.json +17 -0
- package/gateway/ai/content_grounding/fixtures/fail/04_banned_adoption.json +17 -0
- package/gateway/ai/content_grounding/fixtures/fail/05_aggregate_no_numeric.json +17 -0
- package/gateway/ai/content_grounding/fixtures/fail/06_unversioned_inference_rule.json +18 -0
- package/gateway/ai/content_grounding/fixtures/pass/01_feature_shipped.json +18 -0
- package/gateway/ai/content_grounding/fixtures/pass/02_aggregate_claim.json +23 -0
- package/gateway/ai/content_grounding/fixtures/pass/03_attestation.json +16 -0
- package/gateway/ai/content_grounding/schemas/claim.schema.json +40 -0
- package/gateway/ai/content_grounding/schemas/event.schema.json +23 -0
- package/gateway/ai/content_grounding/schemas.py +276 -0
- package/gateway/ai/content_grounding/telemetry.py +221 -0
- package/gateway/ai/governance.py +89 -0
- package/gateway/ai/hot_reload.py +148 -7
- package/gateway/ai/inbox_drafts/__init__.py +61 -0
- package/gateway/ai/inbox_drafts/registry.py +412 -0
- package/gateway/ai/inbox_drafts/schema.py +374 -0
- package/gateway/ai/inbox_executor.py +565 -0
- package/gateway/ai/ledger_manager.py +1483 -25
- package/gateway/ai/license_core.py +3 -1
- package/gateway/ai/mcp_bridge.py +1 -1
- package/gateway/ai/reddit_proxy.py +8 -6
- package/gateway/ai/server.py +451 -9
- package/gateway/ai/supabase_sync.py +47 -7
- package/gateway/ai/swarm.py +1 -1
- package/gateway/ai/workers/executor.py +1 -1
- package/gateway/core/diff_engine_v2.py +45 -10
- package/gateway/core/zero_spec/express_extractor.py +1 -1
- package/lib/delimit-template.js +5 -0
- 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(
|
|
23
|
-
|
|
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 {
|
|
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
|
+
}
|