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
|
@@ -24,7 +24,9 @@ PRO_TOOLS = frozenset({
|
|
|
24
24
|
"delimit_deploy_plan", "delimit_deploy_build", "delimit_deploy_publish",
|
|
25
25
|
"delimit_deploy_verify", "delimit_deploy_rollback", "delimit_deploy_status",
|
|
26
26
|
"delimit_deploy_site", "delimit_deploy_npm",
|
|
27
|
-
|
|
27
|
+
# delimit_memory_store + delimit_memory_recent are FREE (LED-193 — basic
|
|
28
|
+
# store + recent retrieval). Only delimit_memory_search is Pro.
|
|
29
|
+
"delimit_memory_search",
|
|
28
30
|
"delimit_vault_search", "delimit_vault_snapshot", "delimit_vault_health",
|
|
29
31
|
"delimit_evidence_collect", "delimit_evidence_verify",
|
|
30
32
|
"delimit_deliberate", "delimit_models",
|
package/gateway/ai/mcp_bridge.py
CHANGED
|
@@ -28,7 +28,7 @@ class MCPSubprocessClient:
|
|
|
28
28
|
self._request_id = 0
|
|
29
29
|
|
|
30
30
|
def start(self):
|
|
31
|
-
self._proc = subprocess.Popen(
|
|
31
|
+
self._proc = subprocess.Popen( # nosec B-subprocess_shell: MCP bridge spawns user-configured CLI via shell; argv validated upstream
|
|
32
32
|
self.command, shell=True,
|
|
33
33
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
34
34
|
)
|
|
@@ -54,9 +54,10 @@ def fetch_subreddit(subreddit: str, sort: str = "new", limit: int = 10) -> List[
|
|
|
54
54
|
try:
|
|
55
55
|
fetch_url = f"{proxy_url}?url={urllib.parse.quote(reddit_url, safe='')}"
|
|
56
56
|
headers = {"User-Agent": "Delimit/1.0"}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
# nosec B105 — reads proxy auth credential from config, not a hardcoded secret
|
|
58
|
+
auth_token = proxy_cfg.get("token", "")
|
|
59
|
+
if auth_token:
|
|
60
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
|
60
61
|
req = urllib.request.Request(fetch_url, headers=headers)
|
|
61
62
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
62
63
|
body = json.loads(resp.read().decode())
|
|
@@ -100,9 +101,10 @@ def fetch_thread(thread_id: str) -> Optional[Dict[str, Any]]:
|
|
|
100
101
|
try:
|
|
101
102
|
fetch_url = f"{proxy_url}?url={urllib.parse.quote(reddit_url, safe='')}"
|
|
102
103
|
headers = {"User-Agent": "Delimit/1.0"}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
# nosec B105 — reads proxy auth credential from config, not a hardcoded secret
|
|
105
|
+
auth_token = proxy_cfg.get("token", "")
|
|
106
|
+
if auth_token:
|
|
107
|
+
headers["Authorization"] = f"Bearer {auth_token}"
|
|
106
108
|
req = urllib.request.Request(fetch_url, headers=headers)
|
|
107
109
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
108
110
|
data = json.loads(resp.read().decode())
|
package/gateway/ai/server.py
CHANGED
|
@@ -2045,6 +2045,33 @@ def delimit_gov_verify(task_id: str = "", repo: str = ".") -> Dict[str, Any]:
|
|
|
2045
2045
|
return _delimit_gov_impl(action="verify", task_id=task_id, repo=repo)
|
|
2046
2046
|
|
|
2047
2047
|
|
|
2048
|
+
@mcp.tool()
|
|
2049
|
+
def delimit_external_pr_check(
|
|
2050
|
+
repo: str,
|
|
2051
|
+
author: str = "",
|
|
2052
|
+
state: str = "all",
|
|
2053
|
+
) -> Dict[str, Any]:
|
|
2054
|
+
"""Pre-PR duplicate guard for external repos. Run BEFORE drafting.
|
|
2055
|
+
|
|
2056
|
+
Lists existing PRs from `author` against `repo` via gh CLI. Returns
|
|
2057
|
+
fail-closed verdict — any open PR or PR merged in the last 30 days
|
|
2058
|
+
yields verdict='duplicate' so the caller stops drafting before any
|
|
2059
|
+
deliberation or submission work.
|
|
2060
|
+
|
|
2061
|
+
Args:
|
|
2062
|
+
repo: External GitHub repo, e.g. "goharbor/harbor".
|
|
2063
|
+
author: GitHub username to filter by (recommended). Empty = all.
|
|
2064
|
+
state: "open" | "closed" | "merged" | "all". Default "all".
|
|
2065
|
+
"""
|
|
2066
|
+
from backends.governance_bridge import external_pr_check
|
|
2067
|
+
return _safe_call(
|
|
2068
|
+
external_pr_check,
|
|
2069
|
+
repo=repo,
|
|
2070
|
+
author=author or None,
|
|
2071
|
+
state=state,
|
|
2072
|
+
)
|
|
2073
|
+
|
|
2074
|
+
|
|
2048
2075
|
# ─── Memory ─────────────────────────────────────────────────────────────
|
|
2049
2076
|
|
|
2050
2077
|
@mcp.tool()
|
|
@@ -2068,6 +2095,7 @@ def delimit_memory_store(
|
|
|
2068
2095
|
content: str,
|
|
2069
2096
|
tags: Optional[Union[str, List[str]]] = None,
|
|
2070
2097
|
context: Optional[str] = None,
|
|
2098
|
+
hot_load: bool = False,
|
|
2071
2099
|
) -> Dict[str, Any]:
|
|
2072
2100
|
"""Store a memory entry for future retrieval.
|
|
2073
2101
|
|
|
@@ -2078,6 +2106,12 @@ def delimit_memory_store(
|
|
|
2078
2106
|
content: The content to remember.
|
|
2079
2107
|
tags: Optional categorization tags.
|
|
2080
2108
|
context: Optional context about when/why this was stored.
|
|
2109
|
+
hot_load: LED-1165 Phase 2 #5: when True, mark the entry for
|
|
2110
|
+
one-way projection into the Claude Code auto-memory
|
|
2111
|
+
`MEMORY.md` hot-load index. Existing entries default to False
|
|
2112
|
+
(durable in delimit_memory, not projected). The projection
|
|
2113
|
+
writer that consumes this flag is shipped separately as PR-B;
|
|
2114
|
+
this PR only persists the flag.
|
|
2081
2115
|
"""
|
|
2082
2116
|
# LED-193: memory_store is now free (basic store)
|
|
2083
2117
|
try:
|
|
@@ -2085,7 +2119,10 @@ def delimit_memory_store(
|
|
|
2085
2119
|
except ValueError as e:
|
|
2086
2120
|
return _with_next_steps("memory_store", {"error": str(e)})
|
|
2087
2121
|
from backends.memory_bridge import store
|
|
2088
|
-
return _with_next_steps(
|
|
2122
|
+
return _with_next_steps(
|
|
2123
|
+
"memory_store",
|
|
2124
|
+
_safe_call(store, content=content, tags=tags, context=context, hot_load=hot_load),
|
|
2125
|
+
)
|
|
2089
2126
|
|
|
2090
2127
|
|
|
2091
2128
|
@mcp.tool()
|
|
@@ -2102,6 +2139,50 @@ def delimit_memory_recent(limit: int = 5) -> Dict[str, Any]:
|
|
|
2102
2139
|
return _with_next_steps("memory_recent", _safe_call(get_recent, limit=limit))
|
|
2103
2140
|
|
|
2104
2141
|
|
|
2142
|
+
@mcp.tool()
|
|
2143
|
+
def delimit_memory_index(
|
|
2144
|
+
target_path: str = "",
|
|
2145
|
+
dry_run: bool = False,
|
|
2146
|
+
limit: int = 200,
|
|
2147
|
+
) -> Dict[str, Any]:
|
|
2148
|
+
"""Project delimit_memory hot entries into Claude Code's MEMORY.md.
|
|
2149
|
+
|
|
2150
|
+
LED-1165 Phase 2 #5 PR-B. One-way projection of all delimit_memory
|
|
2151
|
+
entries flagged `hot_load=True` into a managed section of the
|
|
2152
|
+
target Markdown file. Lets Claude Code see hot entries on session
|
|
2153
|
+
start without making delimit_memory dependent on Anthropic's
|
|
2154
|
+
auto-memory format.
|
|
2155
|
+
|
|
2156
|
+
Behavior:
|
|
2157
|
+
- If target_path's file has `<!-- delimit:start -->` /
|
|
2158
|
+
`<!-- delimit:end -->` markers, ONLY the content between them
|
|
2159
|
+
is replaced. Anything outside the markers is preserved.
|
|
2160
|
+
- If markers are missing, the managed section is APPENDED to the
|
|
2161
|
+
end of the file (existing content is never touched).
|
|
2162
|
+
- If the file doesn't exist, it's created with just the section.
|
|
2163
|
+
- **One-way projection only.** Reading MEMORY.md back into
|
|
2164
|
+
delimit_memory is explicitly out of scope (Anthropic owns the
|
|
2165
|
+
auto-memory format; format-drift risk).
|
|
2166
|
+
|
|
2167
|
+
Args:
|
|
2168
|
+
target_path: file to write. Empty = default
|
|
2169
|
+
~/.claude/projects/-root/memory/MEMORY.md.
|
|
2170
|
+
dry_run: True returns the rendered content size without writing.
|
|
2171
|
+
limit: cap on entries projected. Default 200.
|
|
2172
|
+
|
|
2173
|
+
Returns:
|
|
2174
|
+
{target, dry_run, entries, wrote_chars or would_write_chars,
|
|
2175
|
+
had_existing_block, had_existing_file, preserved_user_content}
|
|
2176
|
+
"""
|
|
2177
|
+
from backends.memory_bridge import project_to_memory_md
|
|
2178
|
+
from pathlib import Path
|
|
2179
|
+
target = Path(target_path) if target_path else None
|
|
2180
|
+
return _with_next_steps(
|
|
2181
|
+
"memory_index",
|
|
2182
|
+
_safe_call(project_to_memory_md, target_path=target, dry_run=dry_run, limit=limit),
|
|
2183
|
+
)
|
|
2184
|
+
|
|
2185
|
+
|
|
2105
2186
|
# ─── Vault ──────────────────────────────────────────────────────────────
|
|
2106
2187
|
|
|
2107
2188
|
@mcp.tool()
|
|
@@ -5436,26 +5517,309 @@ def delimit_ledger_done(item_id: str, note: str = "", venture: str = "") -> Dict
|
|
|
5436
5517
|
return _with_next_steps("ledger_done", result)
|
|
5437
5518
|
|
|
5438
5519
|
|
|
5520
|
+
@mcp.tool()
|
|
5521
|
+
def delimit_ledger_bulk(
|
|
5522
|
+
item_ids: str,
|
|
5523
|
+
action: str,
|
|
5524
|
+
dry_run: bool = True,
|
|
5525
|
+
note: str = "",
|
|
5526
|
+
new_status: str = "",
|
|
5527
|
+
new_priority: str = "",
|
|
5528
|
+
tag: str = "",
|
|
5529
|
+
venture: str = "",
|
|
5530
|
+
) -> Dict[str, Any]:
|
|
5531
|
+
"""Apply one action to many ledger items in a single call.
|
|
5532
|
+
|
|
5533
|
+
LED-1145 Phase 1 PR-B. Default `dry_run=True` returns what would change
|
|
5534
|
+
without writing — callers must explicitly pass `dry_run=False` to apply.
|
|
5535
|
+
Per-item failures don't block other items in the batch.
|
|
5536
|
+
|
|
5537
|
+
Allowed actions:
|
|
5538
|
+
- archive -> sets status="archived" (soft, replay-preserving)
|
|
5539
|
+
- mark_done -> sets status="done"
|
|
5540
|
+
- cancel -> sets status="cancelled"
|
|
5541
|
+
- set_status -> sets status to `new_status`
|
|
5542
|
+
(one of open/in_progress/blocked/done/cancelled/archived/completed)
|
|
5543
|
+
- set_priority -> sets priority to `new_priority`
|
|
5544
|
+
(one of P0/P1/P2/P3)
|
|
5545
|
+
- add_tag -> appends `tag` if not already present (idempotent)
|
|
5546
|
+
|
|
5547
|
+
NO hard delete. Items remain in the append-only JSONL forever; archive is
|
|
5548
|
+
a status transition that can be reversed via set_status.
|
|
5549
|
+
|
|
5550
|
+
Args:
|
|
5551
|
+
item_ids: comma-separated LED ids (e.g. "LED-915,LED-916,LED-918")
|
|
5552
|
+
or a JSON array of strings.
|
|
5553
|
+
action: one of the actions above.
|
|
5554
|
+
dry_run: True (default) returns `would_change`; False applies and
|
|
5555
|
+
returns `changed`.
|
|
5556
|
+
note: optional note attached to every successful update event.
|
|
5557
|
+
new_status: required when action="set_status".
|
|
5558
|
+
new_priority: required when action="set_priority".
|
|
5559
|
+
tag: required when action="add_tag".
|
|
5560
|
+
venture: project name or path. Auto-detects if empty.
|
|
5561
|
+
"""
|
|
5562
|
+
from ai.ledger_manager import bulk_action
|
|
5563
|
+
project = _resolve_venture(venture)
|
|
5564
|
+
result = bulk_action(
|
|
5565
|
+
item_ids=item_ids,
|
|
5566
|
+
action=action,
|
|
5567
|
+
dry_run=dry_run,
|
|
5568
|
+
note=note or None,
|
|
5569
|
+
new_status=new_status or None,
|
|
5570
|
+
new_priority=new_priority or None,
|
|
5571
|
+
tag=tag or None,
|
|
5572
|
+
project_path=project,
|
|
5573
|
+
)
|
|
5574
|
+
return _with_next_steps("ledger_bulk", result)
|
|
5575
|
+
|
|
5576
|
+
|
|
5577
|
+
@mcp.tool()
|
|
5578
|
+
def delimit_ledger_auto_close_external(
|
|
5579
|
+
venture: str = "",
|
|
5580
|
+
dry_run: bool = True,
|
|
5581
|
+
max_items: int = 200,
|
|
5582
|
+
) -> Dict[str, Any]:
|
|
5583
|
+
"""Walk open ledger items, find ones linked to a GitHub issue/PR, and
|
|
5584
|
+
close LEDs whose external counterpart already resolved.
|
|
5585
|
+
|
|
5586
|
+
LED-1145 Phase 2 #1. Fixes the "ledger drifts from external reality"
|
|
5587
|
+
problem (e.g., LED-1023 stayed open even though goharbor/harbor#23089
|
|
5588
|
+
merged 16 days earlier).
|
|
5589
|
+
|
|
5590
|
+
Detection scans description / context / last_note / tags for:
|
|
5591
|
+
- https://github.com/<owner>/<repo>/(issues|pull)/<num>
|
|
5592
|
+
- <owner>/<repo>#<num> (short form)
|
|
5593
|
+
- gh:<owner>/<repo>/<num> (explicit tag form)
|
|
5594
|
+
|
|
5595
|
+
Action map (per LED-1146 deliberation):
|
|
5596
|
+
- PR with merged=true → mark_done with merge SHA in note
|
|
5597
|
+
- issue/PR closed with state_reason="completed" → mark_done with closed_at
|
|
5598
|
+
- issue/PR closed with state_reason="not_planned" or no reason → archive
|
|
5599
|
+
- state="open" → leave alone
|
|
5600
|
+
- gh API error / 404 → leave alone, recorded in `errors`
|
|
5601
|
+
|
|
5602
|
+
Implementation re-uses bulk_action() under the hood; nothing new on the
|
|
5603
|
+
write path. dry_run=True (default) returns a plan; dry_run=False applies.
|
|
5604
|
+
|
|
5605
|
+
Args:
|
|
5606
|
+
venture: project name or path. Auto-detects if empty.
|
|
5607
|
+
dry_run: True (default) returns a plan without writing.
|
|
5608
|
+
max_items: hard cap on items processed in one call (default 200).
|
|
5609
|
+
When the candidate set exceeds this, the response is `truncated=True`.
|
|
5610
|
+
"""
|
|
5611
|
+
from ai.ledger_manager import auto_close_linked_external
|
|
5612
|
+
project = _resolve_venture(venture)
|
|
5613
|
+
result = auto_close_linked_external(
|
|
5614
|
+
project_path=project,
|
|
5615
|
+
dry_run=dry_run,
|
|
5616
|
+
max_items=max_items,
|
|
5617
|
+
)
|
|
5618
|
+
return _with_next_steps("ledger_auto_close_external", result)
|
|
5619
|
+
|
|
5620
|
+
|
|
5621
|
+
@mcp.tool()
|
|
5622
|
+
def delimit_ledger_groom(
|
|
5623
|
+
venture: str = "",
|
|
5624
|
+
stale_days: int = 30,
|
|
5625
|
+
dup_min_count: int = 3,
|
|
5626
|
+
max_per_category: int = 50,
|
|
5627
|
+
) -> Dict[str, Any]:
|
|
5628
|
+
"""Read-only grooming proposal: surfaces stale, duplicate, and
|
|
5629
|
+
garbage-venture items for the founder to review.
|
|
5630
|
+
|
|
5631
|
+
LED-1145 Phase 2 #2. Risky operations (mass-cancellation, dedup-merge)
|
|
5632
|
+
must NOT be a single atomic action — this tool only PROPOSES; the
|
|
5633
|
+
founder applies via `delimit_ledger_bulk` after review. Each proposal
|
|
5634
|
+
in the response includes a copy-pasteable `ready_to_apply` invocation.
|
|
5635
|
+
|
|
5636
|
+
Categories detected:
|
|
5637
|
+
- stale_open: status open|in_progress|blocked AND updated_at older
|
|
5638
|
+
than `stale_days`. Suggested action: archive.
|
|
5639
|
+
- duplicate_titles: groups of >= `dup_min_count` items sharing the
|
|
5640
|
+
same normalised title prefix (50 chars, [BRACKETED] prefixes
|
|
5641
|
+
stripped, lowercased). Suggested: keep the most-recent item;
|
|
5642
|
+
archive the others.
|
|
5643
|
+
- garbage_venture: items in tmp* / test_* / venture_<letter> /
|
|
5644
|
+
custom-venture buckets. Suggested action: archive.
|
|
5645
|
+
|
|
5646
|
+
Categories NOT detected here (separate tools / future PRs):
|
|
5647
|
+
- linked-external resolved → use `delimit_ledger_auto_close_external`
|
|
5648
|
+
- P0 inflation review
|
|
5649
|
+
- cross-venture orphan cleanup
|
|
5650
|
+
|
|
5651
|
+
Args:
|
|
5652
|
+
venture: project name or path. Auto-detects if empty.
|
|
5653
|
+
stale_days: threshold for stale_open detector (default 30).
|
|
5654
|
+
dup_min_count: minimum group size for duplicate_titles (default 3).
|
|
5655
|
+
max_per_category: cap per category in the response (default 50).
|
|
5656
|
+
"""
|
|
5657
|
+
from ai.ledger_manager import groom_proposal
|
|
5658
|
+
project = _resolve_venture(venture)
|
|
5659
|
+
result = groom_proposal(
|
|
5660
|
+
project_path=project,
|
|
5661
|
+
stale_days=stale_days,
|
|
5662
|
+
dup_min_count=dup_min_count,
|
|
5663
|
+
max_per_category=max_per_category,
|
|
5664
|
+
)
|
|
5665
|
+
return _with_next_steps("ledger_groom", result)
|
|
5666
|
+
|
|
5667
|
+
|
|
5668
|
+
@mcp.tool()
|
|
5669
|
+
def delimit_ledger_auto_cancel_stale(
|
|
5670
|
+
venture: str = "",
|
|
5671
|
+
threshold_days: int = 0,
|
|
5672
|
+
dry_run: bool = True,
|
|
5673
|
+
max_items: int = 200,
|
|
5674
|
+
) -> Dict[str, Any]:
|
|
5675
|
+
"""Auto-archive open ledger items dormant past the stale-TTL threshold.
|
|
5676
|
+
|
|
5677
|
+
LED-1145 Phase 2 #4. Composes the stale-detector logic with
|
|
5678
|
+
`bulk_action(action="archive")` from Phase 1 PR-B. Same dry_run-default
|
|
5679
|
+
safety pattern as `delimit_ledger_auto_close_external` — caller passes
|
|
5680
|
+
`dry_run=False` to apply.
|
|
5681
|
+
|
|
5682
|
+
Distinct from `delimit_ledger_groom`'s stale_open category:
|
|
5683
|
+
- The threshold is stricter (default 60 days vs groom's 30)
|
|
5684
|
+
- It auto-applies on dry_run=False (groom is purely propose)
|
|
5685
|
+
- Intended for nightly automation / scripted cleanup
|
|
5686
|
+
|
|
5687
|
+
Items go through bulk_action(archive) so the no-hard-delete invariant
|
|
5688
|
+
is preserved — the JSONL append-only log keeps the full record.
|
|
5689
|
+
|
|
5690
|
+
Args:
|
|
5691
|
+
venture: Project name or path. Auto-detects if empty.
|
|
5692
|
+
threshold_days: dormancy threshold in days. 0 = read default
|
|
5693
|
+
(60 from STALE_TTL_DEFAULT_DAYS or DELIMIT_STALE_TTL_DAYS env).
|
|
5694
|
+
Pass an int to override.
|
|
5695
|
+
dry_run: True (default) returns the plan; False applies via
|
|
5696
|
+
bulk_action(archive).
|
|
5697
|
+
max_items: cap items processed per call. When the candidate list
|
|
5698
|
+
exceeds this, response includes truncated=True so the caller
|
|
5699
|
+
can run again to drain.
|
|
5700
|
+
"""
|
|
5701
|
+
from ai.ledger_manager import auto_cancel_stale
|
|
5702
|
+
project = _resolve_venture(venture)
|
|
5703
|
+
threshold = threshold_days if threshold_days > 0 else None
|
|
5704
|
+
result = auto_cancel_stale(
|
|
5705
|
+
project_path=project,
|
|
5706
|
+
threshold_days=threshold,
|
|
5707
|
+
dry_run=dry_run,
|
|
5708
|
+
max_items=max_items,
|
|
5709
|
+
)
|
|
5710
|
+
return _with_next_steps("ledger_auto_cancel_stale", result)
|
|
5711
|
+
|
|
5712
|
+
|
|
5713
|
+
@mcp.tool()
|
|
5714
|
+
def delimit_ledger_health(
|
|
5715
|
+
venture: str = "",
|
|
5716
|
+
stale_days: int = 30,
|
|
5717
|
+
dup_min_count: int = 3,
|
|
5718
|
+
) -> Dict[str, Any]:
|
|
5719
|
+
"""One-shot ledger health check — composes list_items + groom_proposal
|
|
5720
|
+
+ the P0 quota helper into a single traffic-light verdict and a list
|
|
5721
|
+
of concrete next actions.
|
|
5722
|
+
|
|
5723
|
+
LED-1145 capstone — closes the loop on the entire ledger-tooling
|
|
5724
|
+
refactor. Designed for nightly/weekly review or session-start status
|
|
5725
|
+
snapshot. Returns:
|
|
5726
|
+
- totals (unresolved / open / in_progress / blocked)
|
|
5727
|
+
- p0 (count vs quota + health)
|
|
5728
|
+
- stale (count >stale_days + health)
|
|
5729
|
+
- duplicates (group count + total items + health)
|
|
5730
|
+
- garbage_venture (count + health)
|
|
5731
|
+
- overall_health (worst-of: green / yellow / red)
|
|
5732
|
+
- next_actions: pre-formatted list of {reason, tool, args, follow_up}
|
|
5733
|
+
|
|
5734
|
+
All Phase 1+2 tools are referenced in the suggested actions, so the
|
|
5735
|
+
response is self-contained for an AI agent that wants to act on it.
|
|
5736
|
+
|
|
5737
|
+
Args:
|
|
5738
|
+
venture: project name or path. Auto-detects if empty.
|
|
5739
|
+
stale_days: stale-detector threshold passed to groom_proposal.
|
|
5740
|
+
dup_min_count: duplicate-detector threshold passed to groom_proposal.
|
|
5741
|
+
"""
|
|
5742
|
+
from ai.ledger_manager import health_summary
|
|
5743
|
+
project = _resolve_venture(venture)
|
|
5744
|
+
result = health_summary(
|
|
5745
|
+
project_path=project,
|
|
5746
|
+
stale_days=stale_days,
|
|
5747
|
+
dup_min_count=dup_min_count,
|
|
5748
|
+
)
|
|
5749
|
+
return _with_next_steps("ledger_health", result)
|
|
5750
|
+
|
|
5751
|
+
|
|
5439
5752
|
@mcp.tool()
|
|
5440
5753
|
def delimit_ledger_list(
|
|
5441
5754
|
venture: str = "",
|
|
5442
5755
|
ledger: str = "both",
|
|
5443
5756
|
status: str = "",
|
|
5444
5757
|
priority: str = "",
|
|
5758
|
+
status_in: str = "",
|
|
5759
|
+
priority_in: str = "",
|
|
5760
|
+
tags_contains_all: str = "",
|
|
5761
|
+
text: str = "",
|
|
5762
|
+
linked_external_id: str = "",
|
|
5763
|
+
created_before: str = "",
|
|
5764
|
+
created_after: str = "",
|
|
5765
|
+
updated_before: str = "",
|
|
5766
|
+
updated_after: str = "",
|
|
5767
|
+
sort: str = "updated_at",
|
|
5768
|
+
order: str = "desc",
|
|
5769
|
+
fields: str = "",
|
|
5445
5770
|
limit: int = 20,
|
|
5771
|
+
cursor: str = "",
|
|
5446
5772
|
) -> Dict[str, Any]:
|
|
5447
5773
|
"""List ledger items for a venture/project.
|
|
5448
5774
|
|
|
5775
|
+
LED-1145 Phase 1 PR-A: extended with multi-value filters, sort + projection,
|
|
5776
|
+
and cursor pagination. Single-value `status` / `priority` remain for
|
|
5777
|
+
backward compatibility — old callers continue to work unchanged.
|
|
5778
|
+
|
|
5449
5779
|
Args:
|
|
5450
5780
|
venture: Project name or path. Auto-detects if empty.
|
|
5451
|
-
ledger: "ops"
|
|
5452
|
-
status:
|
|
5453
|
-
priority:
|
|
5454
|
-
|
|
5781
|
+
ledger: "ops" | "strategy" | "both".
|
|
5782
|
+
status: single-value status filter (back-compat).
|
|
5783
|
+
priority: single-value priority filter (back-compat).
|
|
5784
|
+
status_in: comma-separated statuses (e.g. "open,blocked,in_progress").
|
|
5785
|
+
priority_in: comma-separated priorities (e.g. "P0,P1").
|
|
5786
|
+
tags_contains_all: comma-separated tags; item must contain ALL.
|
|
5787
|
+
text: case-insensitive substring match against title + description.
|
|
5788
|
+
linked_external_id: substring match in description / tags / context
|
|
5789
|
+
(e.g. a github URL, a Linear ticket id, a Discord thread).
|
|
5790
|
+
created_before / created_after / updated_before / updated_after:
|
|
5791
|
+
ISO-8601 timestamp boundaries (e.g. "2026-04-01T00:00:00Z").
|
|
5792
|
+
sort: "updated_at" | "created_at" | "priority". Default updated_at.
|
|
5793
|
+
order: "asc" | "desc". Default desc.
|
|
5794
|
+
fields: response projection. "" or "*" = full (default, back-compat).
|
|
5795
|
+
"slim" = id+title+status+priority+type+venture+updated_at only.
|
|
5796
|
+
CSV of field names = those only. Unknown field names → ERROR.
|
|
5797
|
+
limit: page size (default 20).
|
|
5798
|
+
cursor: opaque pagination token from a prior call's `next_cursor`.
|
|
5799
|
+
Cursor is invalidated when filters change between calls.
|
|
5455
5800
|
"""
|
|
5456
5801
|
from ai.ledger_manager import list_items
|
|
5457
5802
|
project = _resolve_venture(venture)
|
|
5458
|
-
result = list_items(
|
|
5803
|
+
result = list_items(
|
|
5804
|
+
ledger=ledger,
|
|
5805
|
+
status=status or None,
|
|
5806
|
+
priority=priority or None,
|
|
5807
|
+
status__in=status_in or None,
|
|
5808
|
+
priority__in=priority_in or None,
|
|
5809
|
+
tags__contains_all=tags_contains_all or None,
|
|
5810
|
+
text=text or None,
|
|
5811
|
+
linked_external_id=linked_external_id or None,
|
|
5812
|
+
created_before=created_before or None,
|
|
5813
|
+
created_after=created_after or None,
|
|
5814
|
+
updated_before=updated_before or None,
|
|
5815
|
+
updated_after=updated_after or None,
|
|
5816
|
+
sort=sort,
|
|
5817
|
+
order=order,
|
|
5818
|
+
fields=fields or None,
|
|
5819
|
+
limit=limit,
|
|
5820
|
+
cursor=cursor or None,
|
|
5821
|
+
project_path=project,
|
|
5822
|
+
)
|
|
5459
5823
|
return _with_next_steps("ledger_list", result)
|
|
5460
5824
|
|
|
5461
5825
|
|
|
@@ -7876,7 +8240,11 @@ def delimit_changelog(old_spec: str = "", new_spec: str = "", format: str = "mar
|
|
|
7876
8240
|
def delimit_notify(channel: str = "webhook", message: str = "",
|
|
7877
8241
|
webhook_url: str = "", subject: str = "",
|
|
7878
8242
|
event_type: str = "", to: str = "",
|
|
7879
|
-
from_account: str = ""
|
|
8243
|
+
from_account: str = "",
|
|
8244
|
+
draft_kind: str = "",
|
|
8245
|
+
draft_payload: Optional[Union[str, Dict[str, Any]]] = None,
|
|
8246
|
+
draft_target: Optional[Union[str, Dict[str, Any]]] = None,
|
|
8247
|
+
led_ref: str = "") -> Dict[str, Any]:
|
|
7880
8248
|
"""Send a notification (Pro).
|
|
7881
8249
|
|
|
7882
8250
|
IMPORTANT - AUTO-TRIGGER RULE:
|
|
@@ -7905,9 +8273,80 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
7905
8273
|
Send to any address - leave empty for default.
|
|
7906
8274
|
from_account: Sender account key from ~/.delimit/secrets/smtp-all.json
|
|
7907
8275
|
(e.g. 'notifications@example.com'). Email only.
|
|
8276
|
+
|
|
8277
|
+
Optional inbox-executor binding (LED-1129 Phase 1, no auto-execution yet):
|
|
8278
|
+
draft_kind: One of github_comment, social_post, ledger_done,
|
|
8279
|
+
notify_routing_update, deploy_publish_prevalidated_artifact.
|
|
8280
|
+
When set, registers a signed draft in the local SQLite registry
|
|
8281
|
+
so a future executor can match founder Ship-it replies against it.
|
|
8282
|
+
draft_payload: The action contents (e.g. {"body": "..."} for github_comment).
|
|
8283
|
+
JSON string or dict. Required when draft_kind is set.
|
|
8284
|
+
draft_target: Where the action lands (e.g. {"repo":"x/y","issue":1}).
|
|
8285
|
+
JSON string or dict. Required when draft_kind is set.
|
|
8286
|
+
led_ref: Optional LED-XXXX tag tying the draft to its tracking item.
|
|
8287
|
+
Surfaced in subject-line matching by the executor.
|
|
8288
|
+
|
|
8289
|
+
Returns the notify result. When a draft was registered, also includes
|
|
8290
|
+
a `draft` block: {draft_id, draft_kind, signature, registered}.
|
|
7908
8291
|
"""
|
|
7909
8292
|
from ai.notify import send_notification
|
|
7910
|
-
|
|
8293
|
+
|
|
8294
|
+
draft_meta: Optional[Dict[str, Any]] = None
|
|
8295
|
+
if draft_kind:
|
|
8296
|
+
# LED-1129 Phase 1: register a signed draft alongside the email
|
|
8297
|
+
# send. Phase 2 will wire the executor to consume these.
|
|
8298
|
+
try:
|
|
8299
|
+
from ai.inbox_drafts import (
|
|
8300
|
+
DraftKind,
|
|
8301
|
+
insert_draft,
|
|
8302
|
+
sign_draft,
|
|
8303
|
+
)
|
|
8304
|
+
|
|
8305
|
+
# Validate kind against the allowlist enum.
|
|
8306
|
+
try:
|
|
8307
|
+
DraftKind(draft_kind)
|
|
8308
|
+
except ValueError:
|
|
8309
|
+
return _with_next_steps("notify", {
|
|
8310
|
+
"error": (
|
|
8311
|
+
f"draft_kind must be one of "
|
|
8312
|
+
f"{[k.value for k in DraftKind]}; got '{draft_kind}'"
|
|
8313
|
+
),
|
|
8314
|
+
})
|
|
8315
|
+
|
|
8316
|
+
# Coerce string args to dicts for callers that pass JSON strings.
|
|
8317
|
+
payload = _coerce_dict_arg(draft_payload, "draft_payload",
|
|
8318
|
+
string_key="body") if draft_payload is not None else None
|
|
8319
|
+
target = _coerce_dict_arg(draft_target, "draft_target",
|
|
8320
|
+
string_key="target") if draft_target is not None else None
|
|
8321
|
+
|
|
8322
|
+
if payload is None or target is None:
|
|
8323
|
+
return _with_next_steps("notify", {
|
|
8324
|
+
"error": "draft_kind requires both draft_payload and draft_target",
|
|
8325
|
+
})
|
|
8326
|
+
|
|
8327
|
+
signed = sign_draft(
|
|
8328
|
+
draft_kind=draft_kind,
|
|
8329
|
+
target=target,
|
|
8330
|
+
payload=payload,
|
|
8331
|
+
)
|
|
8332
|
+
insert_draft(signed, led_ref=(led_ref or None))
|
|
8333
|
+
draft_meta = {
|
|
8334
|
+
"draft_id": signed.draft_id,
|
|
8335
|
+
"draft_kind": signed.draft_kind,
|
|
8336
|
+
"signature": signed.signature,
|
|
8337
|
+
"registered": True,
|
|
8338
|
+
"led_ref": led_ref or None,
|
|
8339
|
+
}
|
|
8340
|
+
except Exception as e:
|
|
8341
|
+
# Draft registration must not break the notify itself —
|
|
8342
|
+
# the email still goes out, the draft just isn't tracked.
|
|
8343
|
+
# Log the failure in the response so callers can audit.
|
|
8344
|
+
draft_meta = {
|
|
8345
|
+
"registered": False,
|
|
8346
|
+
"error": f"{type(e).__name__}: {e}",
|
|
8347
|
+
}
|
|
8348
|
+
|
|
8349
|
+
result = _safe_call(
|
|
7911
8350
|
send_notification,
|
|
7912
8351
|
channel=channel,
|
|
7913
8352
|
message=message,
|
|
@@ -7916,7 +8355,10 @@ def delimit_notify(channel: str = "webhook", message: str = "",
|
|
|
7916
8355
|
event_type=event_type,
|
|
7917
8356
|
to=to,
|
|
7918
8357
|
from_account=from_account,
|
|
7919
|
-
)
|
|
8358
|
+
)
|
|
8359
|
+
if draft_meta is not None:
|
|
8360
|
+
result["draft"] = draft_meta
|
|
8361
|
+
return _with_next_steps("notify", result)
|
|
7920
8362
|
|
|
7921
8363
|
|
|
7922
8364
|
@mcp.tool()
|
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
"""Supabase sync
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
"""Supabase sync — OPT-IN cloud mirror of local governance events.
|
|
2
|
+
|
|
3
|
+
This module is OFF BY DEFAULT. It only activates if the user supplies BOTH
|
|
4
|
+
of the following:
|
|
5
|
+
- SUPABASE_URL environment variable (or `url` key in ~/.delimit/secrets/supabase.json)
|
|
6
|
+
- SUPABASE_SERVICE_ROLE_KEY env var (or `service_role_key` key in the same file)
|
|
7
|
+
|
|
8
|
+
When activated, it mirrors locally-written events/ledger/work-order/deliberation
|
|
9
|
+
rows into the user's own Supabase project so they can view them in
|
|
10
|
+
app.delimit.ai. The local files under ~/.delimit/ remain the source of truth;
|
|
11
|
+
this is a read-side convenience, never a write-side requirement.
|
|
12
|
+
|
|
13
|
+
Data scope (what gets sent when enabled):
|
|
14
|
+
- events: tool name, timestamp, status, model id, venture tag, session id,
|
|
15
|
+
risk level, trace id, span id. NO source code. NO prompts. NO responses.
|
|
16
|
+
- ledger items: id, title, priority, venture, status, description snippet.
|
|
17
|
+
- work orders: id, metadata fields, status.
|
|
18
|
+
- deliberations: summary metadata.
|
|
19
|
+
|
|
20
|
+
KILL SWITCH:
|
|
21
|
+
Set DELIMIT_DISABLE_CLOUD_SYNC=1 in the environment to disable ALL cloud
|
|
22
|
+
mirroring even if Supabase credentials are present. The local files continue
|
|
23
|
+
to work. This is the same-session runtime equivalent of simply not configuring
|
|
24
|
+
SUPABASE_URL/SUPABASE_SERVICE_ROLE_KEY in the first place.
|
|
25
|
+
|
|
26
|
+
Transport:
|
|
27
|
+
Writes are fire-and-forget. Tool execution never blocks on Supabase
|
|
28
|
+
reachability. All sync_* functions swallow exceptions silently; the
|
|
29
|
+
failure mode is "nothing appears in your dashboard" not "nothing runs."
|
|
30
|
+
|
|
31
|
+
LED-1056: disclosure added per external issue #56 (delimit-ai/delimit-mcp-server).
|
|
5
32
|
"""
|
|
6
33
|
import json
|
|
7
34
|
import os
|
|
@@ -17,8 +44,14 @@ _init_attempted = False
|
|
|
17
44
|
SUPABASE_URL = os.environ.get("SUPABASE_URL", "")
|
|
18
45
|
SUPABASE_KEY = os.environ.get("SUPABASE_SERVICE_ROLE_KEY", "")
|
|
19
46
|
|
|
20
|
-
#
|
|
21
|
-
|
|
47
|
+
# LED-1056: explicit user kill switch. Overrides any credentials the user
|
|
48
|
+
# might have configured. Setting this env var forces ALL sync_* operations
|
|
49
|
+
# to no-op, making cloud sync unconditionally off for the session.
|
|
50
|
+
_CLOUD_SYNC_DISABLED = os.environ.get("DELIMIT_DISABLE_CLOUD_SYNC", "").strip().lower() in ("1", "true", "yes", "on")
|
|
51
|
+
|
|
52
|
+
# Also check local secrets file — only if env vars weren't already provided
|
|
53
|
+
# AND the kill switch is not set.
|
|
54
|
+
if not SUPABASE_URL and not _CLOUD_SYNC_DISABLED:
|
|
22
55
|
secrets_file = Path.home() / ".delimit" / "secrets" / "supabase.json"
|
|
23
56
|
if secrets_file.exists():
|
|
24
57
|
try:
|
|
@@ -57,8 +90,15 @@ def _normalize_venture(value) -> str:
|
|
|
57
90
|
|
|
58
91
|
|
|
59
92
|
def _get_client():
|
|
60
|
-
"""Lazy-init Supabase client. Returns the SDK client, 'http' for fallback, or None.
|
|
93
|
+
"""Lazy-init Supabase client. Returns the SDK client, 'http' for fallback, or None.
|
|
94
|
+
|
|
95
|
+
Returns None (disabled) if:
|
|
96
|
+
- DELIMIT_DISABLE_CLOUD_SYNC=1 is set (user kill switch, LED-1056), OR
|
|
97
|
+
- SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY are not configured.
|
|
98
|
+
"""
|
|
61
99
|
global _client, _init_attempted
|
|
100
|
+
if _CLOUD_SYNC_DISABLED:
|
|
101
|
+
return None
|
|
62
102
|
if _client is not None:
|
|
63
103
|
return _client
|
|
64
104
|
if _init_attempted:
|
package/gateway/ai/swarm.py
CHANGED
|
@@ -846,7 +846,7 @@ def create_tool(
|
|
|
846
846
|
|
|
847
847
|
# Security scan — check for dangerous patterns
|
|
848
848
|
dangerous = [
|
|
849
|
-
"subprocess.call", "os.system", "exec(", "eval(",
|
|
849
|
+
"subprocess.call", "os.system", "exec(", "eval(", # nosec B-eval_usage: MCP tool dispatch — evaluates a whitelisted tool function reference
|
|
850
850
|
"import socket", "import http.server",
|
|
851
851
|
"__import__", "compile(",
|
|
852
852
|
]
|