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
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
|
-
|
|
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
|
[](https://www.npmjs.com/package/delimit-cli)
|
|
19
|
-
[](https://github.com/delimit-ai/delimit-mcp-server)
|
|
20
9
|
[](https://github.com/marketplace/actions/delimit-api-governance)
|
|
21
10
|
[](https://opensource.org/licenses/MIT)
|
|
22
11
|
[](https://glama.ai/mcp/servers/delimit-ai/delimit)
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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://
|
|
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
|
+
}
|
package/bin/delimit-setup.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|