delimit-cli 4.6.0 → 4.6.2

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.
@@ -124,7 +124,16 @@ def _detect_venture(project_path: str = ".") -> Dict[str, str]:
124
124
 
125
125
 
126
126
  def _register_venture(info: Dict[str, str]):
127
- """Silently register a venture in the global registry."""
127
+ """Silently register a venture in the global registry.
128
+
129
+ Phase C follow-up (2026-05-18): reject paths under /tmp/* or the
130
+ bare "/tmp" itself. Pytest tmp_path values leaked into the registry
131
+ as ventures (`tmp: /tmp`, `test_project: /tmp/pytest-of-root/...`),
132
+ causing every fresh tmp_path to match via path-prefix in
133
+ resolve_venture and breaking test_resolve_venture_unregistered_path.
134
+ The guard fails-silently — tests that pass tmp_path to functions
135
+ which auto-register simply don't pollute the registry going forward.
136
+ """
128
137
  GLOBAL_DIR.mkdir(parents=True, exist_ok=True)
129
138
  ventures = {}
130
139
  if VENTURES_FILE.exists():
@@ -134,9 +143,19 @@ def _register_venture(info: Dict[str, str]):
134
143
  pass
135
144
 
136
145
  name = info["name"]
146
+ path = info.get("path", "")
147
+ # Guard against the specific test-state pollution that broke
148
+ # test_resolve_venture_unregistered_path: a `tmp: /tmp` venture
149
+ # caught EVERY pytest tmp_path via path-prefix in resolve_venture.
150
+ # Reject bare "/tmp" only. Deeper /tmp/<X> paths are fine — they
151
+ # only path-prefix-match their own subtree, not every tmp_path,
152
+ # AND legitimate test fixtures (e.g. test_ledger_proof) register
153
+ # subpaths during a single test run and need that to work.
154
+ if path == "/tmp" or path.rstrip("/") == "/tmp":
155
+ return
137
156
  if name not in ventures:
138
157
  ventures[name] = {
139
- "path": info.get("path", ""),
158
+ "path": path,
140
159
  "repo": info.get("repo", ""),
141
160
  "type": info.get("type", ""),
142
161
  "registered_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
@@ -565,8 +584,18 @@ def update_item(
565
584
  blocks: Optional[str] = None,
566
585
  project_path: str = ".",
567
586
  worked_by: str = "",
587
+ commit_sha: Optional[str] = None,
588
+ pr_url: Optional[str] = None,
568
589
  ) -> Dict[str, Any]:
569
- """Update an existing ledger item's fields."""
590
+ """Update an existing ledger item's fields.
591
+
592
+ LED-1408 Phase 1: when `status="done"` is requested, callers MAY provide
593
+ `commit_sha` and/or `pr_url` as proof that the work shipped to main.
594
+ The proof is recorded on the update event under `ship_proof` with a
595
+ `verified: bool` flag. Phase 1 does NOT enforce — items still
596
+ transition to `done` even without proof — but the flag lets future
597
+ audits and the Phase 2 reconciler find unverified-done items.
598
+ """
570
599
  _ensure(project_path)
571
600
  ledger_dir = _project_ledger_dir(project_path)
572
601
 
@@ -633,6 +662,49 @@ def update_item(
633
662
  update["blocked_by"] = blocked_by
634
663
  if blocks:
635
664
  update["blocks"] = blocks
665
+
666
+ # LED-1408 Phase 1: attach ship_proof block when status transitions to
667
+ # `done` or `shipped_pending`. Verified=True iff commit_sha or pr_url
668
+ # was supplied (directly or scraped from the note). Phase 2's
669
+ # reconciler will use this to distinguish "trustworthy done" from
670
+ # "marked done but never verified on main."
671
+ if status in ("done", "shipped_pending"):
672
+ try:
673
+ from ai.ledger_proof import build_ship_proof
674
+ update["ship_proof"] = build_ship_proof(
675
+ commit_sha=commit_sha,
676
+ pr_url=pr_url,
677
+ note=note,
678
+ )
679
+ # LED-1420 Phase 2 strict-mode flip: when DELIMIT_LEDGER_STRICT_DONE=1,
680
+ # an unverified `done` transition is downgraded to `shipped_pending`
681
+ # so the nightly reconciler (scripts/delimit_ledger_reconciler.py)
682
+ # can promote it to `done` once a commit-trailer match shows up on
683
+ # origin/main. Off by default so existing workflows keep closing
684
+ # items without hitting an unexpected gate; flip when the
685
+ # reconciler has been observed running for ~1 week without
686
+ # surprises.
687
+ strict = os.environ.get("DELIMIT_LEDGER_STRICT_DONE") == "1"
688
+ if (
689
+ strict
690
+ and status == "done"
691
+ and not update["ship_proof"].get("verified")
692
+ ):
693
+ update["status"] = "shipped_pending"
694
+ existing_note = update.get("note") or ""
695
+ suffix = (
696
+ "[LED-1420 strict-mode: downgraded done → shipped_pending — "
697
+ "no commit_sha/pr_url proof; reconciler will upgrade to "
698
+ "done when it finds a Ledger-Item: " + item_id + " trailer "
699
+ "on origin/main]"
700
+ )
701
+ update["note"] = (existing_note + " " + suffix).strip() if existing_note else suffix
702
+ except Exception:
703
+ # Soft-fail: a ship_proof bug must not break ledger close.
704
+ # The unverified state will be re-detectable from the missing
705
+ # key on the next audit pass.
706
+ pass
707
+
636
708
  _append(path, update)
637
709
 
638
710
  # Sync to Supabase for dashboard visibility
@@ -1306,7 +1378,12 @@ def session_history(limit: int = 5) -> Dict[str, Any]:
1306
1378
  # `archive` is a soft transition (status="archived", appended to JSONL); items
1307
1379
  # stay in replay forever. NO hard delete. Per-item failures don't block others.
1308
1380
  BULK_ACTIONS = ("archive", "set_status", "set_priority", "add_tag", "mark_done", "cancel")
1309
- _VALID_BULK_STATUSES = ("open", "in_progress", "blocked", "done", "cancelled", "archived", "completed")
1381
+ # LED-1408: `shipped_pending` is the intermediate state between "committed" and
1382
+ # "verified on main." Items transition to shipped_pending when a worker reports
1383
+ # completion (commit exists somewhere) but the orchestrator hasn't yet verified
1384
+ # the commit is reachable from origin/main. The reconciler (Phase 2) promotes
1385
+ # shipped_pending → done once reachability is confirmed.
1386
+ _VALID_BULK_STATUSES = ("open", "in_progress", "blocked", "shipped_pending", "done", "cancelled", "archived", "completed")
1310
1387
  _VALID_BULK_PRIORITIES = ("P0", "P1", "P2", "P3")
1311
1388
 
1312
1389
 
@@ -0,0 +1,127 @@
1
+ """Ledger ship-state proof helpers (LED-1408 Phase 1).
2
+
3
+ When a ledger item transitions to `done`, we want auditable evidence that the
4
+ fix actually shipped. Two forms of proof:
5
+
6
+ 1. **Commit-trailer binding** — the merge commit carries a `Ledger-Item:
7
+ LED-NNNN` trailer. Lets a webhook or reconciler walk `git log
8
+ origin/main` and find every commit-to-ledger link without naming
9
+ conventions or fuzzy matching.
10
+
11
+ 2. **PR-URL linkage** — `https://github.com/<org>/<repo>/pull/<N>` plus a
12
+ verified `merged_at` timestamp. Fallback for items closed without a
13
+ trailer.
14
+
15
+ Phase 1 (this module): parse + record. Items closed with proof get
16
+ `verified: true` on the event; items closed without proof get
17
+ `verified: false` so a future audit can find them.
18
+
19
+ Phase 2 (separate LED): the reconciler enforces stricter semantics —
20
+ items without proof default to `shipped_pending`, not `done`.
21
+
22
+ Memory anchor: feedback_agent_dashboard_done_means_committed_not_merged.md
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from typing import Dict, Optional
29
+
30
+ # Match `Ledger-Item: LED-1234` (case-insensitive, leading whitespace allowed).
31
+ # Pattern intentionally tolerant of trailing whitespace + multiple LED IDs:
32
+ # we extract the FIRST LED-N on the line.
33
+ _LEDGER_TRAILER_RE = re.compile(
34
+ r"(?im)^\s*Ledger-Item\s*:\s*(LED-\d+)",
35
+ )
36
+
37
+ # Match a GitHub PR URL: https://github.com/<owner>/<repo>/pull/<N>
38
+ _PR_URL_RE = re.compile(
39
+ r"https://github\.com/([\w.-]+)/([\w.-]+)/pull/(\d+)",
40
+ )
41
+
42
+
43
+ def parse_ledger_trailer(commit_message: str) -> Optional[str]:
44
+ """Extract the `Ledger-Item: LED-NNNN` trailer value from a commit message.
45
+
46
+ Returns the LED id (e.g. `LED-1408`) or None if no trailer is present.
47
+ The trailer must be on its own line; mentions inside prose (e.g.
48
+ `mentions LED-1408 in passing`) do NOT match.
49
+ """
50
+ if not commit_message:
51
+ return None
52
+ match = _LEDGER_TRAILER_RE.search(commit_message)
53
+ if match:
54
+ return match.group(1)
55
+ return None
56
+
57
+
58
+ def parse_pr_url(text: str) -> Optional[Dict[str, str]]:
59
+ """Extract the first GitHub PR URL from any string.
60
+
61
+ Returns {owner, repo, number} or None.
62
+ """
63
+ if not text:
64
+ return None
65
+ match = _PR_URL_RE.search(text)
66
+ if match:
67
+ return {
68
+ "owner": match.group(1),
69
+ "repo": match.group(2),
70
+ "number": match.group(3),
71
+ }
72
+ return None
73
+
74
+
75
+ def build_ship_proof(
76
+ commit_sha: Optional[str] = None,
77
+ pr_url: Optional[str] = None,
78
+ note: Optional[str] = None,
79
+ ) -> Dict[str, object]:
80
+ """Build a ship-proof block to attach to a ledger `done` event.
81
+
82
+ Inputs may come from explicit MCP-tool args OR from inline mentions
83
+ in the note (the caller might paste a PR URL into the note field
84
+ without realizing it's also queryable).
85
+
86
+ Returns a dict with keys:
87
+ - verified: bool — True iff commit_sha OR pr_url was provided
88
+ - commit_sha: str or None
89
+ - pr_url: str or None
90
+ - pr_owner / pr_repo / pr_number: str or None (parsed from pr_url)
91
+ - ledger_trailer: str or None (parsed from note, if present)
92
+
93
+ The `verified` flag is the primary downstream signal. A future
94
+ reconciler will refuse to transition `done` without verified=True;
95
+ Phase 1 only records the flag without enforcing.
96
+ """
97
+ proof: Dict[str, object] = {
98
+ "verified": bool(commit_sha or pr_url),
99
+ "commit_sha": commit_sha or None,
100
+ "pr_url": pr_url or None,
101
+ }
102
+
103
+ if pr_url:
104
+ parsed = parse_pr_url(pr_url)
105
+ if parsed:
106
+ proof["pr_owner"] = parsed["owner"]
107
+ proof["pr_repo"] = parsed["repo"]
108
+ proof["pr_number"] = parsed["number"]
109
+
110
+ # If pr_url not explicitly passed but appears in the note, capture it
111
+ if not pr_url and note:
112
+ parsed = parse_pr_url(note)
113
+ if parsed:
114
+ proof["pr_url"] = f"https://github.com/{parsed['owner']}/{parsed['repo']}/pull/{parsed['number']}"
115
+ proof["pr_owner"] = parsed["owner"]
116
+ proof["pr_repo"] = parsed["repo"]
117
+ proof["pr_number"] = parsed["number"]
118
+ proof["verified"] = True
119
+
120
+ # Capture any Ledger-Item trailer from the note (rare but possible —
121
+ # a worker might paste the commit message into the close note).
122
+ if note:
123
+ trailer = parse_ledger_trailer(note)
124
+ if trailer:
125
+ proof["ledger_trailer"] = trailer
126
+
127
+ return proof
@@ -20,12 +20,77 @@ try:
20
20
  PRO_TOOLS as _CORE_PRO_TOOLS,
21
21
  FREE_TRIAL_LIMITS,
22
22
  )
23
- # Extend compiled PRO_TOOLS with tools added after last binary build
23
+ # Extend compiled PRO_TOOLS with tools added after last binary build.
24
+ # LED-1260: keep this in lockstep with the fallback set below — any tool
25
+ # in the fallback PRO_TOOLS that's NOT in the compiled set must be added
26
+ # here, otherwise customers with the binary get those tools FREE while
27
+ # customers without the binary pay for them (regression-on-success).
28
+ # The runtime test in tests/test_license.py asserts both sets are
29
+ # equal. LED-1410 makes this stronger: the extension set below is
30
+ # CODEGEN from ai/pro_tools.yaml (same SSoT as the compiled
31
+ # set), so the two are equal by construction. The | union with
32
+ # _CORE_PRO_TOOLS is preserved so OLDER compiled .so files that
33
+ # were built before a YAML addition still pick up the new tool
34
+ # at runtime.
24
35
  PRO_TOOLS = _CORE_PRO_TOOLS | frozenset({
25
- "delimit_social_approve",
26
- # Autonomous build loop
27
- "delimit_next_task", "delimit_task_complete",
28
- "delimit_loop_status", "delimit_loop_config",
36
+ # CODEGEN-START: EXTENSION_PRO_TOOLS
37
+ "delimit_agent_complete",
38
+ "delimit_agent_dispatch",
39
+ "delimit_agent_handoff",
40
+ "delimit_agent_status",
41
+ "delimit_cost_alert",
42
+ "delimit_cost_analyze",
43
+ "delimit_cost_optimize",
44
+ "delimit_deliberate",
45
+ "delimit_deploy_build",
46
+ "delimit_deploy_npm",
47
+ "delimit_deploy_plan",
48
+ "delimit_deploy_publish",
49
+ "delimit_deploy_rollback",
50
+ "delimit_deploy_site",
51
+ "delimit_deploy_status",
52
+ "delimit_deploy_verify",
53
+ "delimit_evidence_collect",
54
+ "delimit_evidence_verify",
55
+ "delimit_executor",
56
+ "delimit_gov_evaluate",
57
+ "delimit_gov_new_task",
58
+ "delimit_gov_policy",
59
+ "delimit_gov_run",
60
+ "delimit_gov_verify",
61
+ "delimit_loop_config",
62
+ "delimit_loop_status",
63
+ "delimit_memory_search",
64
+ "delimit_models",
65
+ "delimit_next_task",
66
+ "delimit_notify",
67
+ "delimit_obs_logs",
68
+ "delimit_obs_metrics",
69
+ "delimit_obs_status",
70
+ "delimit_os_gates",
71
+ "delimit_os_plan",
72
+ "delimit_os_status",
73
+ "delimit_release_plan",
74
+ "delimit_release_status",
75
+ "delimit_release_sync",
76
+ "delimit_repo_analyze",
77
+ "delimit_repo_config_audit",
78
+ "delimit_repo_config_validate",
79
+ "delimit_repo_diagnose",
80
+ "delimit_screen_record",
81
+ "delimit_screenshot",
82
+ "delimit_security_deliberate",
83
+ "delimit_security_ingest",
84
+ "delimit_social_approve",
85
+ "delimit_social_generate",
86
+ "delimit_social_history",
87
+ "delimit_social_post",
88
+ "delimit_task_complete",
89
+ "delimit_test_coverage",
90
+ "delimit_vault_health",
91
+ "delimit_vault_search",
92
+ "delimit_vault_snapshot",
93
+ # CODEGEN-END: EXTENSION_PRO_TOOLS
29
94
  })
30
95
  except ImportError:
31
96
  # license_core not available — three known cases:
@@ -50,49 +115,69 @@ except ImportError:
50
115
 
51
116
  LICENSE_FILE = Path.home() / ".delimit" / "license.json"
52
117
 
118
+ # LED-1410: CODEGEN from ai/pro_tools.yaml — same SSoT as the
119
+ # compiled set above. Memory note preserved here for source readers:
120
+ # delimit_memory_store + delimit_memory_recent are FREE (LED-193).
121
+ # Only delimit_memory_search is Pro.
53
122
  PRO_TOOLS = frozenset({
54
- # Governance deep
55
- "delimit_gov_evaluate", "delimit_gov_policy", "delimit_gov_run", "delimit_gov_verify",
56
- "delimit_gov_new_task",
57
- # OS layer
58
- "delimit_os_plan", "delimit_os_status", "delimit_os_gates",
59
- # Deploy pipeline
60
- "delimit_deploy_plan", "delimit_deploy_build", "delimit_deploy_publish",
61
- "delimit_deploy_verify", "delimit_deploy_rollback", "delimit_deploy_status",
62
- "delimit_deploy_site", "delimit_deploy_npm",
63
- # Memory (search is Pro; store + recent are free)
64
- "delimit_memory_search",
65
- "delimit_vault_search", "delimit_vault_snapshot", "delimit_vault_health",
66
- # Evidence
67
- "delimit_evidence_collect", "delimit_evidence_verify",
68
- # Deliberation + Models
69
- "delimit_deliberate", "delimit_models",
70
- # Security orchestrator
71
- "delimit_security_ingest", "delimit_security_deliberate",
72
- # Observability
73
- "delimit_obs_metrics", "delimit_obs_logs", "delimit_obs_status",
74
- # Release
75
- "delimit_release_plan", "delimit_release_status", "delimit_release_sync",
76
- # Cost
77
- "delimit_cost_analyze", "delimit_cost_optimize", "delimit_cost_alert",
78
- # Social
79
- "delimit_social_post", "delimit_social_generate", "delimit_social_history",
80
- "delimit_social_approve",
81
- # Repo deep
82
- "delimit_repo_analyze", "delimit_repo_config_audit", "delimit_repo_config_validate",
83
- "delimit_repo_diagnose",
84
- # Test
85
- "delimit_test_coverage",
86
- # Screen recording
87
- "delimit_screen_record", "delimit_screenshot",
88
- # Notifications
89
- "delimit_notify",
90
- # Agent orchestration
91
- "delimit_agent_dispatch", "delimit_agent_status",
92
- "delimit_agent_complete", "delimit_agent_handoff",
93
- # Autonomous build loop
94
- "delimit_next_task", "delimit_task_complete",
95
- "delimit_loop_status", "delimit_loop_config",
123
+ # CODEGEN-START: FALLBACK_PRO_TOOLS
124
+ "delimit_agent_complete",
125
+ "delimit_agent_dispatch",
126
+ "delimit_agent_handoff",
127
+ "delimit_agent_status",
128
+ "delimit_cost_alert",
129
+ "delimit_cost_analyze",
130
+ "delimit_cost_optimize",
131
+ "delimit_deliberate",
132
+ "delimit_deploy_build",
133
+ "delimit_deploy_npm",
134
+ "delimit_deploy_plan",
135
+ "delimit_deploy_publish",
136
+ "delimit_deploy_rollback",
137
+ "delimit_deploy_site",
138
+ "delimit_deploy_status",
139
+ "delimit_deploy_verify",
140
+ "delimit_evidence_collect",
141
+ "delimit_evidence_verify",
142
+ "delimit_executor",
143
+ "delimit_gov_evaluate",
144
+ "delimit_gov_new_task",
145
+ "delimit_gov_policy",
146
+ "delimit_gov_run",
147
+ "delimit_gov_verify",
148
+ "delimit_loop_config",
149
+ "delimit_loop_status",
150
+ "delimit_memory_search",
151
+ "delimit_models",
152
+ "delimit_next_task",
153
+ "delimit_notify",
154
+ "delimit_obs_logs",
155
+ "delimit_obs_metrics",
156
+ "delimit_obs_status",
157
+ "delimit_os_gates",
158
+ "delimit_os_plan",
159
+ "delimit_os_status",
160
+ "delimit_release_plan",
161
+ "delimit_release_status",
162
+ "delimit_release_sync",
163
+ "delimit_repo_analyze",
164
+ "delimit_repo_config_audit",
165
+ "delimit_repo_config_validate",
166
+ "delimit_repo_diagnose",
167
+ "delimit_screen_record",
168
+ "delimit_screenshot",
169
+ "delimit_security_deliberate",
170
+ "delimit_security_ingest",
171
+ "delimit_social_approve",
172
+ "delimit_social_generate",
173
+ "delimit_social_history",
174
+ "delimit_social_post",
175
+ "delimit_task_complete",
176
+ "delimit_test_coverage",
177
+ "delimit_vault_health",
178
+ "delimit_vault_search",
179
+ "delimit_vault_snapshot",
180
+ # CODEGEN-END: FALLBACK_PRO_TOOLS
96
181
  })
97
182
  FREE_TRIAL_LIMITS = {"delimit_deliberate": 3}
98
183
 
@@ -7,7 +7,7 @@ import os
7
7
  from pathlib import Path
8
8
  import hashlib
9
9
 
10
- PRO_TOOLS = frozenset({'delimit_gov_evaluate', 'delimit_gov_policy', 'delimit_gov_run', 'delimit_gov_verify', 'delimit_os_plan', 'delimit_os_status', 'delimit_os_gates', 'delimit_deploy_plan', 'delimit_deploy_build', 'delimit_deploy_publish', 'delimit_deploy_verify', 'delimit_deploy_rollback', 'delimit_deploy_status', 'delimit_deploy_site', 'delimit_deploy_npm', 'delimit_memory_search', 'delimit_vault_search', 'delimit_vault_snapshot', 'delimit_vault_health', 'delimit_evidence_collect', 'delimit_evidence_verify', 'delimit_deliberate', 'delimit_models', 'delimit_obs_metrics', 'delimit_obs_logs', 'delimit_obs_status', 'delimit_release_plan', 'delimit_release_status', 'delimit_release_sync', 'delimit_cost_analyze', 'delimit_cost_optimize', 'delimit_cost_alert', 'delimit_social_post', 'delimit_social_generate', 'delimit_social_history', 'delimit_screen_record', 'delimit_screenshot', 'delimit_notify', 'delimit_agent_dispatch', 'delimit_agent_status', 'delimit_agent_complete', 'delimit_agent_handoff', 'delimit_executor'})
10
+ PRO_TOOLS = frozenset({'delimit_agent_complete', 'delimit_agent_dispatch', 'delimit_agent_handoff', 'delimit_agent_status', 'delimit_cost_alert', 'delimit_cost_analyze', 'delimit_cost_optimize', 'delimit_deliberate', 'delimit_deploy_build', 'delimit_deploy_npm', 'delimit_deploy_plan', 'delimit_deploy_publish', 'delimit_deploy_rollback', 'delimit_deploy_site', 'delimit_deploy_status', 'delimit_deploy_verify', 'delimit_evidence_collect', 'delimit_evidence_verify', 'delimit_executor', 'delimit_gov_evaluate', 'delimit_gov_policy', 'delimit_gov_run', 'delimit_gov_verify', 'delimit_memory_search', 'delimit_models', 'delimit_notify', 'delimit_obs_logs', 'delimit_obs_metrics', 'delimit_obs_status', 'delimit_os_gates', 'delimit_os_plan', 'delimit_os_status', 'delimit_release_plan', 'delimit_release_status', 'delimit_release_sync', 'delimit_screen_record', 'delimit_screenshot', 'delimit_social_generate', 'delimit_social_history', 'delimit_social_post', 'delimit_vault_health', 'delimit_vault_search', 'delimit_vault_snapshot'})
11
11
  def needs_revalidation(data: dict) -> bool:
12
12
  ...
13
13
  def revalidate_license(data: dict) -> dict: