delimit-cli 4.5.7 → 4.5.9

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 CHANGED
@@ -219,7 +219,7 @@ Real-world specs can ship malformed shapes. The diff engine now defends against
219
219
 
220
220
  ### Fixed
221
221
  - **Exit-shim counter undercounting** — previously missed commits outside `SESSION_CWD` and dropped Z-suffixed timestamps; both now captured.
222
- - **Proprietary path leaks** — sync-gateway.sh EXCLUDE list hardened to keep Jamsons-portfolio-specific files (social.py, social_target.py, inbox_daemon.py, founding_users.py, deliberation.py) out of the npm bundle.
222
+ - **Proprietary path leaks** — sync-gateway.sh EXCLUDE list hardened to keep portfolio-specific files (social.py, social_target.py, inbox_daemon.py, founding_users.py, deliberation.py) out of the npm bundle.
223
223
 
224
224
  ### Tests
225
225
  - Gateway: 163/163 passing on changed-file tests (social.py, social_target.py, supabase_sync).
@@ -610,8 +610,8 @@ Real-world specs can ship malformed shapes. The diff engine now defends against
610
610
  - Wire local API server into setup flow (STR-057) (223a647d)
611
611
  - release: v3.11.4 — CLAUDE.md auto-update with versioned markers (66db96dd)
612
612
  - security: remove infect.js, hardcoded paths, stale shell scripts (ddb2b1a8)
613
- - security: remove all Jamsons Doctrine references from gateway stubs (1d802a2b)
614
- - security: remove jamsons adapters from public repo (c976fa8a)
613
+ - security: remove all internal doctrine references from gateway stubs (1d802a2b)
614
+ - security: remove holdco adapters from public repo (c976fa8a)
615
615
  - release: v3.11.1 — MCP/AI keywords for npm discoverability (ba984f17)
616
616
  - release: v3.11.0 — agent identity, secrets broker, approval gates (78557ea8)
617
617
  - update: CLI description to match brand positioning (75ff6842)
@@ -623,7 +623,7 @@ Real-world specs can ship malformed shapes. The diff engine now defends against
623
623
  - v3.9.1: download Pro modules from delimit.ai CDN (public URL, no auth needed) (b1462fd4)
624
624
  - v3.9.0: Pro source removed from public package — compiled modules download at install (e4fe7baf)
625
625
  - v3.8.2: Gemini governance trigger + history scrub (116ffb1a)
626
- - security: remove node_modules and jamsons adapters from public repo (f67f3b92)
626
+ - security: remove node_modules and holdco adapters from public repo (f67f3b92)
627
627
  - v3.8.1: governance trigger in all instruction files + MCP server description (7c525231)
628
628
  - v3.7.1: CLI-first deliberation + gateway sync + path cleanup (1555691d)
629
629
  - v3.7.0: cross-model positioning + models configure + release sync (4c9cbcb7)
@@ -698,7 +698,7 @@ Real-world specs can ship malformed shapes. The diff engine now defends against
698
698
  - **LED-061**: [P0] DomainVested: Consistency audit — verdict/flip/action must agree
699
699
  - **LED-062**: [P1] Brand: Add SVG logo to site, favicon, GitHub org avatar, npm
700
700
  - **LED-063**: [P0] Governance trigger shipped in npm — instruction files + MCP description
701
- - **LED-064**: [P0] Security: removed jamsons adapters + node_modules from public repo
701
+ - **LED-064**: [P0] Security: removed holdco adapters + node_modules from public repo
702
702
  - **LED-065**: [P0] ChatOps: Build app.delimit.ai into a unified project management interface
703
703
  - **LED-066**: [P0] Split repos: free tools public, Pro tools private, npm bundles both
704
704
  - **LED-067**: [P0] License: add periodic re-validation (30 day) with 7 day grace period
@@ -1064,7 +1064,7 @@ Real-world specs can ship malformed shapes. The diff engine now defends against
1064
1064
  - GitHub Action smoke test workflow
1065
1065
 
1066
1066
  ### Fixed
1067
- - Gemini deliberation HTTP 400 (ADC credentials + jamsons project)
1067
+ - Gemini deliberation HTTP 400 (ADC credentials + project mismatch)
1068
1068
  - Deliberation timeout: parallelized round 1 (46% faster)
1069
1069
  - Sensor dedup: titles include repo/issue to prevent duplicates
1070
1070
  - Test-mode guard prevents ledger pollution from tests
@@ -8,6 +8,8 @@ Adapter Boundary Contract v1.0:
8
8
  - No schema forking (gateway types are canonical)
9
9
  """
10
10
 
11
+ import os
12
+ import re
11
13
  import sys
12
14
  import json
13
15
  import logging
@@ -16,6 +18,58 @@ from typing import Any, Dict, List, Optional
16
18
 
17
19
  logger = logging.getLogger("delimit.ai.gateway_core")
18
20
 
21
+
22
+ # LED-1265: identity-string redaction filter for changelog output. Patterns are
23
+ # loaded from the DELIMIT_CHANGELOG_REDACT_PATTERNS env var (semicolon-separated
24
+ # regex|replacement pairs, e.g. "FOO|[redacted];BAR|[redacted]"), NOT hardcoded.
25
+ # Why env-var-driven: hardcoding the patterns inline would itself constitute the
26
+ # leak the filter exists to prevent — the patterns must match the strings being
27
+ # redacted, so committing them to source recreates the leak. When the env var is
28
+ # unset (customer machines), this is a no-op pass-through. The internal gateway
29
+ # sets the env var at process start with the exact patterns to scrub before any
30
+ # auto-generated changelog reaches a public surface.
31
+
32
+
33
+ def _load_redaction_patterns() -> List[tuple]:
34
+ """Parse DELIMIT_CHANGELOG_REDACT_PATTERNS env var into compiled patterns.
35
+
36
+ Format: semicolon-separated `regex|replacement` pairs. Empty / unset returns [].
37
+ Invalid regexes are warned and skipped (filter is fail-open: better to skip a
38
+ bad pattern than block all output).
39
+ """
40
+ raw = os.environ.get("DELIMIT_CHANGELOG_REDACT_PATTERNS", "").strip()
41
+ if not raw:
42
+ return []
43
+ patterns = []
44
+ for entry in raw.split(";"):
45
+ entry = entry.strip()
46
+ if not entry or "|" not in entry:
47
+ continue
48
+ pat, _, repl = entry.partition("|")
49
+ try:
50
+ patterns.append((re.compile(pat), repl))
51
+ except re.error as exc:
52
+ logger.warning("ignoring invalid changelog redaction pattern %r: %s", pat, exc)
53
+ return patterns
54
+
55
+
56
+ _IDENTITY_STRING_PATTERNS = _load_redaction_patterns()
57
+
58
+
59
+ def _redact_identity_strings(text: str) -> str:
60
+ """Apply env-var-loaded redaction patterns to text. No-op when env var unset.
61
+
62
+ Defensive filter for changelog generation: immutable commit messages on
63
+ public repos may contain identity strings the auto-generated CHANGELOG would
64
+ re-leak to a fresh public surface. The filter is opt-in per gateway
65
+ deployment via env var; customers don't need to set it.
66
+ """
67
+ if not text or not _IDENTITY_STRING_PATTERNS:
68
+ return text
69
+ for pattern, replacement in _IDENTITY_STRING_PATTERNS:
70
+ text = pattern.sub(replacement, text)
71
+ return text
72
+
19
73
  # Add gateway root to path so we can import core modules
20
74
  GATEWAY_ROOT = Path(__file__).resolve().parent.parent.parent
21
75
  if str(GATEWAY_ROOT) not in sys.path:
@@ -582,6 +636,11 @@ def run_changelog_from_git(
582
636
  ctype = cat
583
637
  break
584
638
 
639
+ # LED-1265: redact founder-holdco identity strings from the commit
640
+ # subject (msg) and author before they enter any output path.
641
+ msg = _redact_identity_strings(msg)
642
+ author = _redact_identity_strings(author)
643
+
585
644
  bucket = ctype if ctype in categories else "other"
586
645
  entry = {"sha": sha[:8], "message": msg, "author": author, "category": bucket}
587
646
  categories[bucket].append(entry)
@@ -641,9 +700,11 @@ def run_changelog_from_git(
641
700
  continue
642
701
  except (ValueError, TypeError):
643
702
  pass # If parsing fails, include the item
703
+ # LED-1265: ledger titles may contain identity strings
704
+ # (e.g. LED items filed before the doctrine bound).
644
705
  ledger_items.append({
645
706
  "id": item_id,
646
- "title": item.get("title", ""),
707
+ "title": _redact_identity_strings(item.get("title", "")),
647
708
  "priority": item.get("priority", ""),
648
709
  })
649
710
  except Exception:
@@ -49,7 +49,7 @@ NON_DELEGABLE_OPERATION_CLASSES = frozenset({
49
49
  "constitutional_rewrite", # edits to founder doctrine canon outside managed sections
50
50
  "authority_class_expansion", # adding a new class of tool / agent / gate
51
51
  "irreversible_capital_commit", # capital commitments above non-delegable threshold
52
- "venture_kill", # shutting down a Jamsons venture
52
+ "venture_kill", # shutting down an internal venture
53
53
  "permission_escalation", # granting elevated access (sudo, admin, write-as-other)
54
54
  "public_truth_claim", # public statement / marketing assertion outrunning evidence
55
55
  })
@@ -0,0 +1,61 @@
1
+ """LED-193 autonomous daemon (MVP).
2
+
3
+ Cron-spawn, stateless, append-only audit. Picks ledger items tagged
4
+ ``auto_execute=class_a:<profile>`` and executes a deterministic profile
5
+ (``format_fix``, ``lockfile_refresh``, ``docs_typo``) on a feature branch.
6
+ NEVER merges. Opens a PR for human review only after local pre-push gates
7
+ pass (security_audit + test_smoke + lint when applicable).
8
+
9
+ Panel decision (UNANIMOUS, 2026-05-07):
10
+ `/home/delimit/delimit-private/deliberations/2026-05-07-led-193-autonomous-daemon-shape.md`
11
+
12
+ Design siblings the cron pattern of LED-1264 ``scan_bridge``:
13
+ - cron-spawn (no long-running process)
14
+ - lockfile concurrency=1
15
+ - append-only audit log
16
+ - kill switch via env var
17
+ - circuit breakers (consecutive failures, daily caps)
18
+
19
+ Public entry points:
20
+
21
+ - :func:`picker.pick_next_item` — ledger-item selection
22
+ - :func:`executor.execute_item` — profile dispatch
23
+ - :func:`gate.run_pre_push_gate` — local pre-push validation
24
+ - :func:`audit.log_execution` — append-only execution log
25
+ - :func:`pause.is_paused` / :func:`pause.pause` / :func:`pause.clear`
26
+ - :func:`cost.check_caps` / :func:`cost.record_run`
27
+
28
+ The cron entry is :mod:`scripts.led193_cron`. Founder applies the
29
+ crontab line manually after review (NOT auto-installed).
30
+ """
31
+
32
+ from ai.led193_daemon.audit import log_execution
33
+ from ai.led193_daemon.cost import check_caps, record_run
34
+ from ai.led193_daemon.executor import execute_item
35
+ from ai.led193_daemon.gate import run_pre_push_gate
36
+ from ai.led193_daemon.pause import clear as clear_pause
37
+ from ai.led193_daemon.pause import is_paused, pause as pause_daemon
38
+ from ai.led193_daemon.picker import pick_next_item
39
+
40
+ # Re-export the submodules so callers can do
41
+ # ``from ai.led193_daemon import audit, cost, executor, gate, pause, picker``
42
+ # without the function-named exports above shadowing the ``pause`` module.
43
+ from ai.led193_daemon import audit, cost, executor, gate, pause, picker # noqa: E402,F401
44
+
45
+ __all__ = [
46
+ "audit",
47
+ "check_caps",
48
+ "clear_pause",
49
+ "cost",
50
+ "execute_item",
51
+ "executor",
52
+ "gate",
53
+ "is_paused",
54
+ "log_execution",
55
+ "pause",
56
+ "pause_daemon",
57
+ "pick_next_item",
58
+ "picker",
59
+ "record_run",
60
+ "run_pre_push_gate",
61
+ ]
@@ -0,0 +1,174 @@
1
+ """LED-193 append-only execution audit log.
2
+
3
+ Every pickup attempt logs one JSON line — success or failure — so
4
+ incidents can be replayed against the daemon's actual behaviour.
5
+
6
+ Schema:
7
+ {
8
+ "ts": ISO8601 UTC,
9
+ "item_id": str,
10
+ "profile": "format_fix" | "lockfile_refresh" | "docs_typo" | "",
11
+ "branch": str | "", # auto/{profile}-{item_id}-{short_hash}
12
+ "pr_url": str | "", # populated only on success
13
+ "result": "success" | "failed" | "noop" | "skipped" | "ci_failed_after_open",
14
+ "reason": str, # human-readable detail
15
+ "cost_estimate": float, # USD; 0.0 for deterministic profiles
16
+ "files_changed": int,
17
+ # optional, populated when known:
18
+ "elapsed_s": float,
19
+ "gate_results": dict,
20
+ }
21
+
22
+ The log is append-only — never rewritten. Path:
23
+ ``~/.delimit/led193_executions.jsonl``
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ from datetime import datetime, timezone
32
+ from pathlib import Path
33
+ from typing import Any, Dict, Optional
34
+
35
+ logger = logging.getLogger("delimit.ai.led193_daemon.audit")
36
+
37
+ AUDIT_LOG = Path.home() / ".delimit" / "led193_executions.jsonl"
38
+
39
+ VALID_RESULTS = {
40
+ "success",
41
+ "failed",
42
+ "noop",
43
+ "skipped",
44
+ "ci_failed_after_open",
45
+ }
46
+
47
+
48
+ def log_execution(
49
+ *,
50
+ item_id: str,
51
+ profile: str = "",
52
+ branch: str = "",
53
+ pr_url: str = "",
54
+ result: str = "failed",
55
+ reason: str = "",
56
+ cost_estimate: float = 0.0,
57
+ files_changed: int = 0,
58
+ elapsed_s: Optional[float] = None,
59
+ gate_results: Optional[Dict[str, Any]] = None,
60
+ audit_log_path: Optional[Path] = None,
61
+ ) -> Dict[str, Any]:
62
+ """Write one append-only audit line.
63
+
64
+ Returns the record actually written (useful for tests + the cron
65
+ summary). Best-effort — a write failure logs a warning but never
66
+ raises (the daemon must not crash on disk-full).
67
+ """
68
+ if result not in VALID_RESULTS:
69
+ # Don't reject — coerce to "failed" with a clarifying reason so
70
+ # we never silently drop an audit row over a typo in caller code.
71
+ reason = f"invalid_result={result!r}; original_reason={reason!r}"
72
+ result = "failed"
73
+
74
+ record: Dict[str, Any] = {
75
+ "ts": datetime.now(timezone.utc).isoformat(),
76
+ "item_id": item_id,
77
+ "profile": profile,
78
+ "branch": branch,
79
+ "pr_url": pr_url,
80
+ "result": result,
81
+ "reason": reason,
82
+ "cost_estimate": float(cost_estimate),
83
+ "files_changed": int(files_changed),
84
+ }
85
+ if elapsed_s is not None:
86
+ record["elapsed_s"] = round(float(elapsed_s), 3)
87
+ if gate_results is not None:
88
+ record["gate_results"] = gate_results
89
+
90
+ target = audit_log_path or AUDIT_LOG
91
+ try:
92
+ target.parent.mkdir(parents=True, exist_ok=True)
93
+ with target.open("a", encoding="utf-8") as fh:
94
+ fh.write(json.dumps(record, ensure_ascii=False) + "\n")
95
+ except OSError as exc: # pragma: no cover — best-effort
96
+ logger.warning("led193_daemon: failed to write audit log %s: %s", target, exc)
97
+
98
+ return record
99
+
100
+
101
+ def recent_results(
102
+ *,
103
+ audit_log_path: Optional[Path] = None,
104
+ limit: int = 100,
105
+ ) -> list:
106
+ """Read the most recent N records from the audit log (newest first).
107
+
108
+ Used by the consecutive-failures circuit breaker and by the cron
109
+ summary. Returns ``[]`` when the file doesn't exist or is empty.
110
+ """
111
+ target = audit_log_path or AUDIT_LOG
112
+ if not target.exists():
113
+ return []
114
+ try:
115
+ lines = target.read_text(encoding="utf-8").splitlines()
116
+ except OSError:
117
+ return []
118
+ out = []
119
+ for line in reversed(lines):
120
+ line = line.strip()
121
+ if not line:
122
+ continue
123
+ try:
124
+ out.append(json.loads(line))
125
+ except (json.JSONDecodeError, ValueError):
126
+ continue
127
+ if len(out) >= limit:
128
+ break
129
+ return out
130
+
131
+
132
+ def consecutive_failures(
133
+ *,
134
+ audit_log_path: Optional[Path] = None,
135
+ ) -> int:
136
+ """Count CONSECUTIVE failures from the most recent record backward.
137
+
138
+ Stops at the first non-failure (success/noop/skipped). Used by the
139
+ 3-strike circuit breaker. ``ci_failed_after_open`` counts as a
140
+ failure for breaker purposes — if the daemon keeps opening PRs that
141
+ break CI, that's a signal to pause.
142
+ """
143
+ failures = 0
144
+ for rec in recent_results(audit_log_path=audit_log_path, limit=20):
145
+ if rec.get("result") in ("failed", "ci_failed_after_open"):
146
+ failures += 1
147
+ continue
148
+ break
149
+ return failures
150
+
151
+
152
+ def prs_opened_today(
153
+ *,
154
+ audit_log_path: Optional[Path] = None,
155
+ now: Optional[datetime] = None,
156
+ ) -> int:
157
+ """Count successful PRs opened in the last 24 hours.
158
+
159
+ Used by the action-volume circuit breaker (max 5 PRs / day).
160
+ """
161
+ now = now or datetime.now(timezone.utc)
162
+ cutoff = now.timestamp() - 86400.0
163
+ n = 0
164
+ for rec in recent_results(audit_log_path=audit_log_path, limit=200):
165
+ if rec.get("result") != "success" or not rec.get("pr_url"):
166
+ continue
167
+ ts = rec.get("ts") or ""
168
+ try:
169
+ rec_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
170
+ if rec_dt.timestamp() >= cutoff:
171
+ n += 1
172
+ except (ValueError, TypeError):
173
+ continue
174
+ return n
@@ -0,0 +1,133 @@
1
+ """LED-193 cost tracking + circuit breakers.
2
+
3
+ MVP profiles are deterministic (no LLM): cost is always 0.0. The cost
4
+ infrastructure exists ahead of time so when Class C ``bounded_patch``
5
+ graduates, the breakers are wired and unit-tested rather than bolted
6
+ on under pressure.
7
+
8
+ Hard caps (panel-locked):
9
+ - Per-item LLM cost: $2 (DELIMIT_LED193_PER_ITEM_USD override)
10
+ - Daily LLM cost: $10 (DELIMIT_LED193_DAILY_USD override)
11
+
12
+ Daily window = trailing 24h, summed from the audit log
13
+ (``cost_estimate`` field). Per-item is enforced by callers BEFORE
14
+ incurring the cost — exceeded → return ``CapTriggered`` and the executor
15
+ short-circuits.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import os
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ from ai.led193_daemon.audit import recent_results
26
+
27
+ DEFAULT_PER_ITEM_USD = 2.00
28
+ DEFAULT_DAILY_USD = 10.00
29
+
30
+
31
+ def per_item_cap() -> float:
32
+ raw = os.environ.get("DELIMIT_LED193_PER_ITEM_USD", "")
33
+ if raw:
34
+ try:
35
+ v = float(raw)
36
+ if v >= 0:
37
+ return v
38
+ except (TypeError, ValueError):
39
+ pass
40
+ return DEFAULT_PER_ITEM_USD
41
+
42
+
43
+ def daily_cap() -> float:
44
+ raw = os.environ.get("DELIMIT_LED193_DAILY_USD", "")
45
+ if raw:
46
+ try:
47
+ v = float(raw)
48
+ if v >= 0:
49
+ return v
50
+ except (TypeError, ValueError):
51
+ pass
52
+ return DEFAULT_DAILY_USD
53
+
54
+
55
+ def daily_spend(
56
+ *,
57
+ audit_log_path: Optional[Path] = None,
58
+ now: Optional[datetime] = None,
59
+ ) -> float:
60
+ """Sum of cost_estimate across audit records in the last 24h."""
61
+ now = now or datetime.now(timezone.utc)
62
+ cutoff = now.timestamp() - 86400.0
63
+ total = 0.0
64
+ for rec in recent_results(audit_log_path=audit_log_path, limit=500):
65
+ ts = rec.get("ts") or ""
66
+ try:
67
+ rec_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
68
+ except (ValueError, TypeError):
69
+ continue
70
+ if rec_dt.timestamp() < cutoff:
71
+ continue
72
+ try:
73
+ total += float(rec.get("cost_estimate") or 0.0)
74
+ except (TypeError, ValueError):
75
+ continue
76
+ return total
77
+
78
+
79
+ def check_caps(
80
+ *,
81
+ estimated_cost: float = 0.0,
82
+ audit_log_path: Optional[Path] = None,
83
+ now: Optional[datetime] = None,
84
+ ) -> dict:
85
+ """Return ``{"ok": bool, "reason": str, ...}``.
86
+
87
+ Caller passes ``estimated_cost`` for the proposed item; we check
88
+ BOTH the per-item cap AND the projected daily total. Deterministic
89
+ profiles pass ``estimated_cost=0.0`` and always return ``ok=True``
90
+ unless the daily cap is already breached (which would only happen
91
+ under a misconfigured override).
92
+ """
93
+ per_cap = per_item_cap()
94
+ if estimated_cost > per_cap:
95
+ return {
96
+ "ok": False,
97
+ "reason": f"per_item_cap_exceeded: ${estimated_cost:.2f} > ${per_cap:.2f}",
98
+ "estimated_cost": estimated_cost,
99
+ "per_item_cap": per_cap,
100
+ }
101
+ spent = daily_spend(audit_log_path=audit_log_path, now=now)
102
+ d_cap = daily_cap()
103
+ projected = spent + estimated_cost
104
+ if projected > d_cap:
105
+ return {
106
+ "ok": False,
107
+ "reason": f"daily_cap_exceeded: ${spent:.2f} + ${estimated_cost:.2f} > ${d_cap:.2f}",
108
+ "daily_spend": spent,
109
+ "daily_cap": d_cap,
110
+ "estimated_cost": estimated_cost,
111
+ }
112
+ return {
113
+ "ok": True,
114
+ "reason": "",
115
+ "daily_spend": spent,
116
+ "daily_cap": d_cap,
117
+ "per_item_cap": per_cap,
118
+ "estimated_cost": estimated_cost,
119
+ }
120
+
121
+
122
+ def record_run(actual_cost: float) -> float:
123
+ """Pass-through for callers that want to declare an actual cost.
124
+
125
+ The actual cost lands in the audit log via the ``cost_estimate``
126
+ field on the record. This helper exists so executor call-sites read
127
+ consistently. Returns the validated, clamped cost.
128
+ """
129
+ try:
130
+ v = float(actual_cost)
131
+ except (TypeError, ValueError):
132
+ return 0.0
133
+ return max(0.0, v)