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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
1
1
  # Changelog
2
2
 
3
+
4
+ ## [4.5.0] - 2026-04-27
5
+
6
+ ### Added — Ledger hygiene toolkit (LED-1145, 7 PRs)
7
+
8
+ The full hygiene loop is now a single MCP surface — call `delimit_ledger_health` to see what's wrong, then run the suggested tool to fix it.
9
+
10
+ - **`delimit_ledger_health`** — one-shot traffic-light status (P0 inflation / stale / duplicates / garbage venture) with ranked next-action list
11
+ - **`delimit_ledger_groom`** — read-only proposal: stale-open + duplicate-titles + garbage-venture detection. Each proposal includes a copy-pasteable `delimit_ledger_bulk` invocation
12
+ - **`delimit_ledger_bulk(item_ids, action, dry_run=True)`** — batch operations (`archive`, `set_status`, `set_priority`, `add_tag`, `mark_done`, `cancel`). `dry_run=True` is the safety default. **No hard delete** — archive is an append-only soft transition
13
+ - **`delimit_ledger_auto_close_external`** — find LEDs linked to a GitHub issue/PR and auto-close when the upstream resolves (mark_done for merged PRs / closed-as-completed; archive for not_planned closures)
14
+ - **`delimit_ledger_auto_cancel_stale`** — auto-archive items dormant past `DELIMIT_STALE_TTL_DAYS` (default 60). Composes `bulk_action(archive)`. dry_run default
15
+ - **Extended `delimit_ledger_list`** — new filters: `status_in`, `priority_in`, `tags_contains_all`, `text`, `linked_external_id`, `created_before/after`, `updated_before/after`, `sort`, `order`, `fields` projection (`"slim"` / explicit list / unknown=error), cursor pagination
16
+ - **P0 soft-quota** on `delimit_ledger_add` — soft warning when count > `DELIMIT_P0_SOFT_QUOTA` (default 50). Item still added; surfaces a nudge to groom
17
+
18
+ ### Added — Memory-system convergence (LED-1165)
19
+
20
+ `delimit_memory` is now the canonical durable memory; Claude Code auto-memory becomes a one-way client projection.
21
+
22
+ - **`hot_load: bool` parameter** on `delimit_memory_store` — opt-in, default False. Marks an entry for projection
23
+ - **`delimit_memory_index(target_path, dry_run, limit)`** — projects hot_load=True entries into a managed section of MEMORY.md (`<!-- delimit:start -->` / `<!-- delimit:end -->` markers). User content outside markers is preserved verbatim. **One-way only** — never reads MEMORY.md back into delimit_memory
24
+
25
+ ### Fixed — Secret-scanner false positives in `tools_infra`
26
+
27
+ The `_CREDENTIAL_FALSE_POSITIVES` regex now suppresses three additional benign patterns so legitimate credential-loading code (env-var lookup, dict-getter, control-flow guards) doesn't trip the audit:
28
+ - `\w+\.get(` (any object-method getter, was tokens-only)
29
+ - `if not <var>:` (Python control-flow with credential variable)
30
+ - `:\n` matched-text (block-opener colon, not key-value separator)
31
+
32
+ ### Fixed — OpenAPI diff engine defensive coverage
33
+
34
+ Real-world specs can ship malformed shapes. The diff engine now defends against the entire dict-iteration crash class without losing any actual finding:
35
+
36
+ - **`required: bool` in object schemas** (legal in parameter objects but seen leaking into nested schemas) — was raising `TypeError: 'bool' object is not iterable`. Treats as no-required-fields and continues
37
+ - **`properties: [...]` instead of dict** (Kong-class) — `.keys()` no longer crashes; treats as empty properties and continues
38
+ - **`paths: []`, `responses: []`, `content: []` (request and response)** — all coerce to `{}` at function entry. Diffs against the well-formed side still produce correct findings
39
+
40
+ ### Tests
41
+ - 108 ledger-manager tests (15 originals + 93 new across 7 features + E2E hygiene-loop integration)
42
+ - 24 memory-bridge tests (new test file)
43
+ - 60 diff-engine tests (49 + 11 new defensive)
44
+ - All backward-compatible — existing test suites pass without modification
45
+
46
+ ### Backward compatibility
47
+ - All MCP tool parameter additions are optional with safe defaults
48
+ - Existing storage format is unchanged (`hot_load` and `archived` are additive on the entry/status side)
49
+ - No CLI command renamed or removed
50
+ - Default `delimit_ledger_list` response shape preserved (full record by default; `fields="slim"` opts into the 90% payload reduction)
51
+
52
+ ## [4.4.0] - 2026-04-25
53
+
54
+ ### Added — Pre-external-PR duplicate guard
55
+
56
+ - **New MCP tool `delimit_external_pr_check(repo, author)`** — pre-flight gate that runs `gh pr list` against a target repo and returns a fail-closed verdict if any matching open PR (or recently-merged PR within 30 days) already exists. Prevents autonomous workflows from drafting duplicate external-repo PRs off stale outreach signals.
57
+ - **`delimit_gov_evaluate(action="external_pr", context={target_repo, author})`** composes the duplicate check + policy evaluation in one call. Verdict `blocked_duplicate` is a hard stop.
58
+ - **CLAUDE.md auto-trigger rule added**: BEFORE drafting any external-repo PR, the duplicate gate must pass.
59
+ - 12 new unit tests covering: open PR, recently-merged PR, old merged PR, closed PR, gh CLI not installed, gh auth failure, missing target_repo. End-to-end test against real `goharbor/harbor` PR #23089 confirms duplicate detection works.
60
+
61
+ ### Documentation
62
+ - readme: replace stale demo links + delete old GIF (#67) (e6059dd3)
63
+ - LED-1089 SECURITY.md — install-time + runtime threat model (#65) (4e03319c)
64
+ - readme: canon-aligned hero + real attestation demo (#63) (9172924a)
65
+
66
+ ### CI/CD
67
+ - retry verify-published-version with bounded backoff (fixes v4.3.4 race) (#62) (f5a768d5)
68
+
69
+ ### Other
70
+ - LED-1084 week 2 hygiene: build features.json during `delimit setup` (#66) (8145456f)
71
+ - sync: gateway LED-1010 design tool fixes (Tailwind awareness, status taxonomy, link-href FP) (#64) (d756e1bb)
72
+
73
+ ### Completed Ledger Items
74
+ - **LED-1094**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
75
+ - **LED-1095**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
76
+ - **LED-1096**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
77
+ - **LED-1097**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
78
+ - **LED-1098**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
79
+ - **LED-1100**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
80
+ - **LED-1102**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
81
+ - **LED-1103**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
82
+ - **LED-1104**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
83
+ - **LED-1105**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
84
+ - **LED-1106**: [P1] Build responsive-overflow gate (narrow MVP) — dogfood on delimit.ai
85
+ - **LED-1107**: [P1] Fix 3 responsive overflow bugs caught by LED-1106 gate on first dogfood run
86
+ - **LED-1108**: [P1] Cross-venture task
87
+ - **LED-1109**: [P1] Cross-venture task
88
+ - **LED-1110**: [P1] Cross-venture task
89
+ - **LED-1111**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
90
+ - **LED-1113**: [P1] Cross-venture task
91
+ - **LED-1114**: [P1] Consensus reached: Review the full transcript. As orchestrator, provide your own analysis and final
92
+
93
+ ### Stats
94
+ - **Commits**: 6
95
+ - **Files changed**: 6
96
+ - **Insertions**: 708(+) / 130(-)
97
+ - **Since**: v4.3.4
98
+
3
99
  ## [4.2.0] - 2026-04-21
4
100
 
5
101
  ### Added (gateway sync — LED-987 through LED-1008)
package/README.md CHANGED
@@ -1,36 +1,43 @@
1
1
  # `</>` Delimit
2
2
 
3
- Stop re-explaining your codebase every session. Memory, tasks, and governance that persist across Claude Code, Codex, Cursor, and Gemini CLI.
3
+ **The merge gate for AI-written code with signed, replayable attestation.**
4
4
 
5
-
6
- ---
7
-
8
- ## Think and Build
9
-
10
- The universal command for the Delimit Swarm. When you say **"Think and Build"**, your AI agents (Claude, Codex, Gemini, Cursor) automatically deploy a background autonomous build loop that monitors your ledger, deliberates on strategy, and implements code while you focus on the architecture.
11
-
12
- - **"Think"**: Trigger multi-model deliberation and strategic dispatch.
13
- - **"Build"**: Activate the background daemon to execute tasks and verify gates.
14
- - **"Vault"**: Manage local secrets and API keys (AES-256 encrypted).
15
-
16
- Works across any configuration — from a single model on a budget to an enterprise swarm of 4+ models.
5
+ Wrap any AI coding assistant (Claude Code, Codex, Cursor, Gemini CLI) with a governance chain that runs your gates, records what changed, and signs a replayable receipt for every merge.
17
6
 
18
7
  [![npm](https://img.shields.io/npm/v/delimit-cli)](https://www.npmjs.com/package/delimit-cli)
19
- [![Tests](https://img.shields.io/badge/tests-134%20passing-brightgreen)](https://github.com/delimit-ai/delimit-mcp-server)
8
+ [![Tests](https://img.shields.io/badge/tests-165%20passing-brightgreen)](https://github.com/delimit-ai/delimit-mcp-server)
20
9
  [![GitHub Action](https://img.shields.io/badge/GitHub%20Action-v1.6.0-blue)](https://github.com/marketplace/actions/delimit-api-governance)
21
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
22
11
  [![Glama](https://glama.ai/mcp/servers/delimit-ai/delimit/badge)](https://glama.ai/mcp/servers/delimit-ai/delimit)
23
12
 
24
- <p align="center">
25
- <img src="docs/demo.gif" alt="Delimit v4.20 doctor, simulate, status, memory" width="700">
26
- </p>
13
+ ```console
14
+ $ delimit wrap -- claude "fix the flaky test in tests/api.spec.ts"
15
+
16
+ ✓ repo_diagnose
17
+ ✓ security_audit 0 critical · 0 secrets
18
+ ✓ test_smoke 165/165
19
+ ✓ changed_files 1
20
+ ✓ attestation signed att_a05050eb8e13277e
21
+ delimit.attestation.v1 · HMAC-SHA256
22
+ replay → https://delimit.ai/att/att_a05050eb8e13277e
23
+ ```
24
+
25
+ Every wrapped run emits a `delimit.attestation.v1` bundle: repo head before/after, changed files, gate results, HMAC-SHA256 signature, and a replay URL. Advisory by default; flip to enforcing when you're ready.
27
26
 
28
27
  <p align="center">
29
- <a href="https://youtu.be/8e_6P7rkFxo">Watch the demo</a> · <a href="https://youtu.be/4O1wY4vWmiY">Multi-model deliberation</a> · <a href="https://delimit.ai">Website</a>
28
+ <a href="https://github.com/delimit-ai/delimit-action/releases/tag/v1.10.0">See a signed release</a> · <a href="https://delimit.ai/docs/workflow">Workflow guide</a> · <a href="https://delimit.ai">Website</a>
30
29
  </p>
31
30
 
32
31
  ---
33
32
 
33
+ ## Think and Build
34
+
35
+ Beyond the merge gate, Delimit orchestrates multi-model deliberation and autonomous builds. `delimit think` dispatches a strategic question to Claude, Codex, Gemini, and Grok; `delimit build` activates a background daemon that executes ledger tasks through the gate chain. `delimit vault` manages local secrets (AES-256).
36
+
37
+ Works across any configuration, from a single model on a budget to a full panel.
38
+
39
+ ---
40
+
34
41
  ## Try it in 2 minutes
35
42
 
36
43
  ```bash
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Delimit Security Skill for Codex CLI
4
+ *
5
+ * Validates that Codex-generated code doesn't introduce security anti-patterns.
6
+ * Runs as a security validation skill.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const SECURITY_PATTERNS = [
13
+ { pattern: /eval\s*\(/g, severity: 'high', message: 'eval() usage detected — potential code injection' }, // nosec B-eval_usage: regex-pattern DEFINITION string for security scanner
14
+ { pattern: /exec\s*\(/g, severity: 'medium', message: 'exec() usage — verify input sanitization' }, // nosec B-exec_usage: regex-pattern DEFINITION string for security scanner
15
+ { pattern: /shell\s*=\s*True/g, severity: 'high', message: 'subprocess with shell=True — command injection risk' },
16
+ { pattern: /dangerouslySetInnerHTML/g, severity: 'medium', message: 'dangerouslySetInnerHTML — XSS risk' }, // nosec B-dangerous_innerHTML: regex-pattern DEFINITION string
17
+ { pattern: /password\s*=\s*["'][^"']+["']/gi, severity: 'high', message: 'Hardcoded password detected' },
18
+ { pattern: /api[_-]?key\s*=\s*["'][A-Za-z0-9]{10,}["']/gi, severity: 'high', message: 'Hardcoded API key detected' },
19
+ ];
20
+
21
+ function checkSecurity(context) {
22
+ const code = context.code || context.content || '';
23
+ if (!code) return { status: 'clean', findings: [] };
24
+
25
+ const findings = [];
26
+ for (const { pattern, severity, message } of SECURITY_PATTERNS) {
27
+ const matches = code.match(pattern);
28
+ if (matches) {
29
+ findings.push({ severity, message, count: matches.length });
30
+ }
31
+ }
32
+
33
+ // Audit
34
+ try {
35
+ const auditDir = path.join(process.env.HOME || '', '.delimit', 'audit');
36
+ fs.mkdirSync(auditDir, { recursive: true });
37
+ const record = {
38
+ timestamp: new Date().toISOString(),
39
+ source: 'codex-security',
40
+ findings_count: findings.length,
41
+ high_count: findings.filter(f => f.severity === 'high').length,
42
+ };
43
+ const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
44
+ fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
45
+ } catch {}
46
+
47
+ const hasHigh = findings.some(f => f.severity === 'high');
48
+ return {
49
+ status: hasHigh ? 'flagged' : findings.length > 0 ? 'warnings' : 'clean',
50
+ findings,
51
+ };
52
+ }
53
+
54
+ const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
55
+ const result = checkSecurity(context);
56
+
57
+ if (result.status === 'flagged') {
58
+ for (const f of result.findings) {
59
+ console.error(`[Delimit Security] ${f.severity.toUpperCase()}: ${f.message}`);
60
+ }
61
+ process.exit(1);
62
+ }
63
+
64
+ process.exit(0);
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Delimit Governance Skill for Codex CLI
4
+ *
5
+ * Runs as a validation skill triggered on pre-code-generation and pre-suggestion.
6
+ * Checks governance state and policy compliance before Codex executes actions.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const DELIMIT_HOME = path.join(process.env.HOME || '', '.delimit');
13
+ const MODE_FILE = path.join(DELIMIT_HOME, 'enforcement_mode');
14
+
15
+ function getMode() {
16
+ try {
17
+ return fs.readFileSync(MODE_FILE, 'utf-8').trim();
18
+ } catch {
19
+ return 'guarded'; // Default
20
+ }
21
+ }
22
+
23
+ function checkGovernance(context) {
24
+ const mode = getMode();
25
+ const warnings = [];
26
+
27
+ // Check if governance is initialized
28
+ const policiesFile = path.join(process.cwd(), '.delimit', 'policies.yml');
29
+ if (!fs.existsSync(policiesFile)) {
30
+ warnings.push('No .delimit/policies.yml — run: delimit init');
31
+ }
32
+
33
+ // Check for sensitive file access
34
+ const sensitivePatterns = ['.env', 'credentials', '.ssh', 'secrets'];
35
+ const target = context.target || context.file || '';
36
+ for (const pattern of sensitivePatterns) {
37
+ if (target.includes(pattern)) {
38
+ if (mode === 'enforce') {
39
+ return { status: 'blocked', reason: `Access to sensitive path: ${target}` };
40
+ }
41
+ warnings.push(`Accessing sensitive path: ${target}`);
42
+ }
43
+ }
44
+
45
+ // Audit log
46
+ try {
47
+ const auditDir = path.join(DELIMIT_HOME, 'audit');
48
+ fs.mkdirSync(auditDir, { recursive: true });
49
+ const record = {
50
+ timestamp: new Date().toISOString(),
51
+ source: 'codex-skill',
52
+ mode,
53
+ context: typeof context === 'object' ? JSON.stringify(context).slice(0, 200) : String(context).slice(0, 200),
54
+ warnings,
55
+ };
56
+ const auditFile = path.join(auditDir, `${new Date().toISOString().split('T')[0]}.jsonl`);
57
+ fs.appendFileSync(auditFile, JSON.stringify(record) + '\n');
58
+ } catch {}
59
+
60
+ return { status: 'allowed', mode, warnings };
61
+ }
62
+
63
+ // Entry point — read context from stdin or args
64
+ const context = process.argv[2] ? JSON.parse(process.argv[2]) : {};
65
+ const result = checkGovernance(context);
66
+
67
+ if (result.status === 'blocked') {
68
+ console.error(`[Delimit] BLOCKED: ${result.reason}`);
69
+ process.exit(1);
70
+ }
71
+
72
+ if (result.warnings.length > 0) {
73
+ for (const w of result.warnings) {
74
+ console.error(`[Delimit] Warning: ${w}`);
75
+ }
76
+ }
77
+
78
+ process.exit(0);
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Delimit Governance Rules for Cursor
4
+ *
5
+ * Cursor doesn't have a hook system like Claude Code or Codex,
6
+ * so governance enforcement happens server-side via MCP tool calls.
7
+ * This adapter manages the .cursorrules and .cursor/rules/ files
8
+ * that guide Cursor's behavior.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ // LED-213: Import canonical template for cross-model parity
15
+ const { getDelimitSection } = require('../lib/delimit-template');
16
+
17
+ const HOME = process.env.HOME || '';
18
+ const CURSOR_DIR = path.join(HOME, '.cursor');
19
+ const CURSOR_RULES_DIR = path.join(CURSOR_DIR, 'rules');
20
+ const CURSORRULES_FILE = path.join(HOME, '.cursorrules');
21
+
22
+ /**
23
+ * Install Delimit governance rules into Cursor.
24
+ * Creates both .cursorrules (legacy) and .cursor/rules/delimit.md (new).
25
+ */
26
+ function installRules(version) {
27
+ const rules = getDelimitRules(version);
28
+
29
+ // Install to .cursor/rules/delimit.md (new location, Cursor 0.45+)
30
+ if (fs.existsSync(CURSOR_DIR)) {
31
+ fs.mkdirSync(CURSOR_RULES_DIR, { recursive: true });
32
+ const rulesFile = path.join(CURSOR_RULES_DIR, 'delimit.md');
33
+ fs.writeFileSync(rulesFile, rules);
34
+ }
35
+
36
+ return { installed: true, paths: [CURSORRULES_FILE, path.join(CURSOR_RULES_DIR, 'delimit.md')] };
37
+ }
38
+
39
+ /**
40
+ * Remove Delimit rules from Cursor.
41
+ */
42
+ function uninstallRules() {
43
+ const removed = [];
44
+
45
+ // Remove from .cursor/rules/
46
+ const rulesFile = path.join(CURSOR_RULES_DIR, 'delimit.md');
47
+ if (fs.existsSync(rulesFile)) {
48
+ fs.unlinkSync(rulesFile);
49
+ removed.push(rulesFile);
50
+ }
51
+
52
+ return { removed };
53
+ }
54
+
55
+ function getDelimitRules(version) {
56
+ // LED-213: Use canonical Consensus 123 template for Cursor parity
57
+ return getDelimitSection();
58
+ }
59
+
60
+ module.exports = { installRules, uninstallRules, getDelimitRules };
61
+
62
+ // CLI entry point
63
+ if (require.main === module) {
64
+ const action = process.argv[2] || 'install';
65
+ const version = process.argv[3] || '3.11.9';
66
+ if (action === 'install') {
67
+ const result = installRules(version);
68
+ console.log(`Installed Delimit rules to Cursor: ${result.paths.join(', ')}`);
69
+ } else if (action === 'uninstall') {
70
+ const result = uninstallRules();
71
+ console.log(`Removed: ${result.removed.join(', ') || 'nothing to remove'}`);
72
+ }
73
+ }
@@ -273,6 +273,29 @@ async function main() {
273
273
  }
274
274
  }
275
275
 
276
+ // LED-1084 week 2: build the content-grounding feature whitelist.
277
+ // Populates ~/.delimit/content/grounding/features.json with every
278
+ // @mcp.tool() entry + every CLI subcommand we ship. Drafters then
279
+ // have a concrete list of "things delimit actually does" to check
280
+ // generated text against. Fail-soft — install continues even if
281
+ // the build fails for any reason.
282
+ try {
283
+ const groundingModule = path.join(DELIMIT_HOME, 'server', 'ai', 'content_grounding', 'features.py');
284
+ if (fs.existsSync(groundingModule)) {
285
+ execSync(`"${python}" -m ai.content_grounding.features build`, {
286
+ stdio: 'pipe',
287
+ cwd: path.join(DELIMIT_HOME, 'server'),
288
+ env: { ...process.env, PYTHONPATH: path.join(DELIMIT_HOME, 'server') },
289
+ timeout: 10000,
290
+ });
291
+ await logp(` ${green('✓')} Grounding whitelist built (~/.delimit/content/grounding/features.json)`);
292
+ }
293
+ } catch {
294
+ // Silent fail — the whitelist is optional; drafters fall back to empty
295
+ // which makes the gate strict but doesn't break anything. Customers
296
+ // can rebuild manually later: `python -m ai.content_grounding.features build`.
297
+ }
298
+
276
299
  // Step 3: Configure Claude Code MCP
277
300
  step(3, 'Configuring Claude Code MCP...');
278
301
 
@@ -158,9 +158,49 @@ def _not_init_response(tool_name: str, **extra) -> Dict[str, Any]:
158
158
 
159
159
 
160
160
  def evaluate_trigger(action: str, context: Optional[Dict] = None, repo: str = ".") -> Dict[str, Any]:
161
- """Evaluate if governance is required for an action."""
161
+ """Evaluate if governance is required for an action.
162
+
163
+ Special action "external_pr" runs the duplicate-PR pre-flight check
164
+ (gov.external_pr_check) before any other governance evaluation.
165
+ Context for external_pr should include {"target_repo": "owner/name",
166
+ "author": "github-username"} (author optional but recommended).
167
+ Verdict "duplicate" is propagated to callers as a hard stop.
168
+ """
162
169
  if not _is_initialized(repo):
163
170
  return _not_init_response("gov.evaluate", action=action, repo=repo)
171
+
172
+ # Pre-flight: external PR submission must check for duplicates first
173
+ if action == "external_pr":
174
+ target_repo = (context or {}).get("target_repo")
175
+ if not target_repo:
176
+ return {
177
+ "tool": "gov.evaluate",
178
+ "status": "evaluated",
179
+ "action": action,
180
+ "verdict": "missing_input",
181
+ "error": "external_pr action requires context.target_repo",
182
+ }
183
+ author = (context or {}).get("author")
184
+ check = external_pr_check(repo=target_repo, author=author)
185
+ # If duplicate found, fail-closed: callers should not proceed
186
+ if check.get("verdict") == "duplicate":
187
+ return {
188
+ "tool": "gov.evaluate",
189
+ "status": "evaluated",
190
+ "action": action,
191
+ "verdict": "blocked_duplicate",
192
+ "external_pr_check": check,
193
+ "next_action": (
194
+ "STOP. A matching PR already exists. Review existing PR(s); "
195
+ "do not draft, deliberate, or submit a duplicate."
196
+ ),
197
+ }
198
+ # No duplicate: still surface the check so the caller can audit
199
+ # the chain. Falls through to normal policy evaluation below.
200
+ external_check_result = check
201
+ else:
202
+ external_check_result = None
203
+
164
204
  # Governance is initialized -- evaluate against loaded policy
165
205
  repo_path = Path(repo).resolve()
166
206
  policies_file = repo_path / ".delimit" / "policies.yml"
@@ -169,7 +209,7 @@ def evaluate_trigger(action: str, context: Optional[Dict] = None, repo: str = ".
169
209
  rules = data.get("rules", []) if isinstance(data, dict) else []
170
210
  except Exception:
171
211
  rules = []
172
- return {
212
+ result = {
173
213
  "tool": "gov.evaluate",
174
214
  "status": "evaluated",
175
215
  "action": action,
@@ -178,6 +218,11 @@ def evaluate_trigger(action: str, context: Optional[Dict] = None, repo: str = ".
178
218
  "governance_required": len(rules) > 0,
179
219
  "active_rules": len(rules),
180
220
  }
221
+ if external_check_result is not None:
222
+ result["external_pr_check"] = external_check_result
223
+ result["verdict"] = "ready_for_deliberation"
224
+ result["next_action"] = "Run delimit_deliberate, then submit the PR."
225
+ return result
181
226
 
182
227
 
183
228
  def new_task(title: str, scope: str, risk_level: str = "medium", repo: str = ".") -> Dict[str, Any]:
@@ -241,3 +286,124 @@ def require_owner_approval(context: str, repo: str = ".") -> Dict[str, Any]:
241
286
  "approval_required": False,
242
287
  "context": context,
243
288
  }
289
+
290
+
291
+ def external_pr_check(
292
+ repo: str,
293
+ author: Optional[str] = None,
294
+ state: str = "all",
295
+ ) -> Dict[str, Any]:
296
+ """Pre-PR duplicate guard. Lists existing PRs from `author` against `repo`.
297
+
298
+ Returned verdict is fail-closed: any matching PR (open OR recently merged
299
+ within 30 days OR pending review) returns verdict='duplicate' so callers
300
+ can stop drafting before deliberation/submission.
301
+
302
+ Args:
303
+ repo: External GitHub repo, e.g. "goharbor/harbor".
304
+ author: GitHub username to filter by. If None, lists all PRs.
305
+ state: "open" | "closed" | "merged" | "all". Default "all" (broad).
306
+
307
+ Returns dict with:
308
+ verdict: "no_duplicate" | "duplicate" | "gh_unavailable" | "error"
309
+ prs: List of matching PR records (number, state, reviewDecision, url, mergedAt)
310
+ guidance: Human-readable next-step hint.
311
+ """
312
+ if not repo or "/" not in repo:
313
+ return {
314
+ "tool": "gov.external_pr_check",
315
+ "verdict": "error",
316
+ "error": "repo must be in 'owner/name' format",
317
+ }
318
+
319
+ cmd = ["gh", "pr", "list", "--repo", repo, "--state", state, "--limit", "50",
320
+ "--json", "number,title,state,reviewDecision,createdAt,mergedAt,url,author"]
321
+ if author:
322
+ cmd.extend(["--author", author])
323
+
324
+ try:
325
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
326
+ except FileNotFoundError:
327
+ return {
328
+ "tool": "gov.external_pr_check",
329
+ "verdict": "gh_unavailable",
330
+ "error": "gh CLI not installed",
331
+ "guidance": "Install gh (https://cli.github.com) or run `gh pr list --repo "
332
+ f"{repo}" + (f" --author {author}" if author else "") + "` manually before drafting.",
333
+ }
334
+ except subprocess.TimeoutExpired:
335
+ return {
336
+ "tool": "gov.external_pr_check",
337
+ "verdict": "error",
338
+ "error": "gh pr list timed out after 20s",
339
+ }
340
+
341
+ if proc.returncode != 0:
342
+ # Most common reason: not authenticated
343
+ stderr = (proc.stderr or "").strip()[:300]
344
+ return {
345
+ "tool": "gov.external_pr_check",
346
+ "verdict": "error",
347
+ "error": f"gh pr list failed: {stderr}",
348
+ "guidance": "Run `gh auth status` to verify authentication.",
349
+ }
350
+
351
+ try:
352
+ prs = json.loads(proc.stdout or "[]")
353
+ except json.JSONDecodeError as exc:
354
+ return {
355
+ "tool": "gov.external_pr_check",
356
+ "verdict": "error",
357
+ "error": f"could not parse gh output: {exc}",
358
+ }
359
+
360
+ # Fail-closed criteria: any open PR, or any PR merged in the last 30 days
361
+ import time as _time
362
+ from datetime import datetime, timezone, timedelta
363
+ cutoff = datetime.now(timezone.utc) - timedelta(days=30)
364
+
365
+ duplicates = []
366
+ for pr in prs:
367
+ is_open = pr.get("state") == "OPEN"
368
+ merged_at = pr.get("mergedAt")
369
+ recently_merged = False
370
+ if merged_at:
371
+ try:
372
+ # gh returns ISO8601 like "2026-04-10T11:15:13Z"
373
+ ts = datetime.fromisoformat(merged_at.replace("Z", "+00:00"))
374
+ recently_merged = ts >= cutoff
375
+ except Exception:
376
+ pass
377
+ if is_open or recently_merged:
378
+ duplicates.append({
379
+ "number": pr.get("number"),
380
+ "title": pr.get("title"),
381
+ "state": pr.get("state"),
382
+ "reviewDecision": pr.get("reviewDecision"),
383
+ "createdAt": pr.get("createdAt"),
384
+ "mergedAt": merged_at,
385
+ "url": pr.get("url"),
386
+ "author": (pr.get("author") or {}).get("login"),
387
+ })
388
+
389
+ if duplicates:
390
+ return {
391
+ "tool": "gov.external_pr_check",
392
+ "verdict": "duplicate",
393
+ "repo": repo,
394
+ "author": author,
395
+ "prs": duplicates,
396
+ "guidance": (
397
+ f"Found {len(duplicates)} existing PR(s) — DO NOT draft a duplicate. "
398
+ "Review the existing PR(s) and decide: monitor, comment, or update."
399
+ ),
400
+ }
401
+
402
+ return {
403
+ "tool": "gov.external_pr_check",
404
+ "verdict": "no_duplicate",
405
+ "repo": repo,
406
+ "author": author,
407
+ "checked_count": len(prs),
408
+ "guidance": "Safe to proceed with drafting. Run delimit_deliberate before submitting.",
409
+ }