delimit-cli 3.14.27 → 3.14.29

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 (48) hide show
  1. package/bin/delimit-setup.js +19 -2
  2. package/gateway/ai/backends/deploy_bridge.py +56 -2
  3. package/gateway/ai/backends/gateway_core.py +212 -1
  4. package/gateway/ai/backends/generate_bridge.py +84 -13
  5. package/gateway/ai/backends/governance_bridge.py +63 -16
  6. package/gateway/ai/backends/memory_bridge.py +77 -76
  7. package/gateway/ai/backends/ops_bridge.py +76 -6
  8. package/gateway/ai/backends/os_bridge.py +23 -3
  9. package/gateway/ai/backends/repo_bridge.py +156 -17
  10. package/gateway/ai/backends/tools_design.py +116 -9
  11. package/gateway/ai/backends/tools_infra.py +200 -72
  12. package/gateway/ai/backends/tools_real.py +8 -0
  13. package/gateway/ai/backends/ui_bridge.py +115 -5
  14. package/gateway/ai/backends/vault_bridge.py +69 -114
  15. package/gateway/ai/content_engine.py +1276 -0
  16. package/gateway/ai/context_fs.py +193 -0
  17. package/gateway/ai/daemon.py +500 -0
  18. package/gateway/ai/data_plane.py +291 -0
  19. package/gateway/ai/deliberation.py +1033 -6
  20. package/gateway/ai/events.py +39 -0
  21. package/gateway/ai/founding_users.py +162 -0
  22. package/gateway/ai/governance.py +698 -4
  23. package/gateway/ai/inbox_daemon.py +78 -17
  24. package/gateway/ai/integrations/__init__.py +1 -0
  25. package/gateway/ai/integrations/opensage_wrapper.py +288 -0
  26. package/gateway/ai/key_resolver.py +95 -0
  27. package/gateway/ai/ledger_manager.py +289 -1
  28. package/gateway/ai/license.py +62 -4
  29. package/gateway/ai/license_core.py +208 -7
  30. package/gateway/ai/local_server.py +215 -0
  31. package/gateway/ai/loop_engine.py +408 -0
  32. package/gateway/ai/mcp_bridge.py +178 -0
  33. package/gateway/ai/release_sync.py +2 -2
  34. package/gateway/ai/screen_record.py +374 -0
  35. package/gateway/ai/secrets_broker.py +235 -0
  36. package/gateway/ai/social.py +189 -27
  37. package/gateway/ai/social_target.py +1635 -0
  38. package/gateway/ai/supabase_sync.py +190 -0
  39. package/gateway/ai/tracing.py +195 -0
  40. package/gateway/core/contract_ledger.py +1 -1
  41. package/gateway/core/dependency_graph.py +1 -1
  42. package/gateway/core/dependency_manifest.py +1 -1
  43. package/gateway/core/diff_engine_v2.py +272 -78
  44. package/gateway/core/event_backbone.py +2 -2
  45. package/gateway/core/event_schema.py +1 -1
  46. package/gateway/core/impact_analyzer.py +1 -1
  47. package/gateway/core/policy_engine.py +4 -0
  48. package/package.json +1 -1
@@ -74,14 +74,31 @@ function findSpecFiles(dir, depth = 0) {
74
74
  }
75
75
 
76
76
  async function main() {
77
+ // Self-update check: ensure we're running the latest version
78
+ const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
79
+ try {
80
+ const latest = execSync('npm view delimit-cli version 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
81
+ if (latest && latest !== _pkg.version && latest > _pkg.version) {
82
+ log(dim(` Updating delimit-cli ${_pkg.version} -> ${latest}...`));
83
+ execSync('npm install -g delimit-cli@latest 2>/dev/null', { stdio: 'pipe', timeout: 30000 });
84
+ // Re-exec with the updated version
85
+ const setupPath = path.join(__dirname, 'delimit-setup.js');
86
+ if (fs.existsSync(setupPath)) {
87
+ execSync(`node "${setupPath}"`, { stdio: 'inherit' });
88
+ process.exit(0);
89
+ }
90
+ }
91
+ } catch { /* offline or timeout — continue with current version */ }
92
+
77
93
  log('');
78
94
  log(purple(' ____ ________ ______ _____________'));
79
95
  log(purple(' / __ \\/ ____/ / / _/ |/ / _/_ __/'));
80
96
  log(magenta(' / / / / __/ / / / // /|_/ // / / / '));
81
97
  log(magenta(' / /_/ / /___/ /____/ // / / // / / / '));
82
98
  log(orange('/_____/_____/_____/___/_/ /_/___/ /_/ '));
83
- const _pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
84
- log(dim(` v${_pkg.version}`));
99
+ // Re-read in case we self-updated
100
+ const _pkgNow = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
101
+ log(dim(` v${_pkgNow.version}`));
85
102
  log('');
86
103
 
87
104
  // Step 1: Check prerequisites
@@ -106,11 +106,46 @@ def build(app: str, git_ref: Optional[str] = None) -> Dict[str, Any]:
106
106
 
107
107
 
108
108
  def publish(app: str, git_ref: Optional[str] = None) -> Dict[str, Any]:
109
- """Update latest plan status to published."""
109
+ """Update latest plan status to published after basic readiness checks."""
110
110
  plans = _list_plans(app=app)
111
111
  if not plans:
112
112
  return {"error": f"No deploy plans found for {app}"}
113
113
  latest = plans[0]
114
+ current_status = latest.get("status", "unknown")
115
+ if current_status == "published":
116
+ return {
117
+ "app": app,
118
+ "plan_id": latest["plan_id"],
119
+ "status": "already_published",
120
+ "message": f"Latest plan {latest['plan_id']} is already published.",
121
+ }
122
+ if current_status == "rolled_back":
123
+ return {
124
+ "app": app,
125
+ "plan_id": latest["plan_id"],
126
+ "status": "invalid_state",
127
+ "message": f"Latest plan {latest['plan_id']} was rolled back and cannot be republished.",
128
+ "current_status": current_status,
129
+ }
130
+ if current_status not in {"planned", "built", "verified"}:
131
+ return {
132
+ "app": app,
133
+ "plan_id": latest["plan_id"],
134
+ "status": "invalid_state",
135
+ "message": f"Latest plan {latest['plan_id']} is not ready to publish from status '{current_status}'.",
136
+ "current_status": current_status,
137
+ }
138
+
139
+ build_result = build(app=app, git_ref=git_ref or latest.get("git_ref"))
140
+ if build_result.get("status") != "ready":
141
+ return {
142
+ "app": app,
143
+ "plan_id": latest["plan_id"],
144
+ "status": "not_ready",
145
+ "message": build_result.get("message", "Build prerequisites are not satisfied."),
146
+ "build_status": build_result.get("status"),
147
+ }
148
+
114
149
  now = datetime.now(timezone.utc).isoformat()
115
150
  latest["status"] = "published"
116
151
  latest["updated_at"] = now
@@ -136,11 +171,30 @@ def verify(app: str, env: str, git_ref: Optional[str] = None) -> Dict[str, Any]:
136
171
 
137
172
 
138
173
  def rollback(app: str, env: str, to_sha: Optional[str] = None) -> Dict[str, Any]:
139
- """Mark latest plan as rolled back."""
174
+ """Mark latest published plan as rolled back."""
140
175
  plans = _list_plans(app=app, env=env)
141
176
  if not plans:
142
177
  return {"error": f"No deploy plans found for {app} in {env}"}
143
178
  latest = plans[0]
179
+ current_status = latest.get("status", "unknown")
180
+ if current_status == "rolled_back":
181
+ return {
182
+ "app": app,
183
+ "env": env,
184
+ "plan_id": latest["plan_id"],
185
+ "status": "already_rolled_back",
186
+ "rolled_back_to": latest.get("rolled_back_to"),
187
+ }
188
+ if current_status != "published":
189
+ return {
190
+ "app": app,
191
+ "env": env,
192
+ "plan_id": latest["plan_id"],
193
+ "status": "not_ready",
194
+ "message": f"Cannot roll back plan {latest['plan_id']} from status '{current_status}'. Publish it first.",
195
+ "current_status": current_status,
196
+ }
197
+
144
198
  now = datetime.now(timezone.utc).isoformat()
145
199
  latest["status"] = "rolled_back"
146
200
  latest["updated_at"] = now
@@ -9,6 +9,7 @@ Adapter Boundary Contract v1.0:
9
9
  """
10
10
 
11
11
  import sys
12
+ import json
12
13
  import logging
13
14
  from pathlib import Path
14
15
  from typing import Any, Dict, List, Optional
@@ -23,7 +24,6 @@ if str(GATEWAY_ROOT) not in sys.path:
23
24
 
24
25
  def _load_specs(spec_path: str) -> Dict[str, Any]:
25
26
  """Load an OpenAPI spec from a file path."""
26
- import json
27
27
  import yaml
28
28
 
29
29
  p = Path(spec_path)
@@ -36,6 +36,55 @@ def _load_specs(spec_path: str) -> Dict[str, Any]:
36
36
  return json.loads(content)
37
37
 
38
38
 
39
+ def _read_jsonl(path: Path) -> List[Dict[str, Any]]:
40
+ """Read JSONL entries from a file, skipping malformed lines."""
41
+ items: List[Dict[str, Any]] = []
42
+ if not path.exists():
43
+ return items
44
+ try:
45
+ with open(path, "r", encoding="utf-8") as handle:
46
+ for line in handle:
47
+ stripped = line.strip()
48
+ if not stripped:
49
+ continue
50
+ try:
51
+ payload = json.loads(stripped)
52
+ except json.JSONDecodeError:
53
+ continue
54
+ if isinstance(payload, dict):
55
+ items.append(payload)
56
+ except OSError:
57
+ return []
58
+ return items
59
+
60
+
61
+ def _query_project_ledger_fallback(ledger_path: Path) -> Optional[Dict[str, Any]]:
62
+ """Fallback for project-local ledgers that use operations/strategy jsonl files."""
63
+ if ledger_path.name != "events.jsonl":
64
+ return None
65
+
66
+ ledger_dir = ledger_path.parent
67
+ operations = _read_jsonl(ledger_dir / "operations.jsonl")
68
+ strategy = _read_jsonl(ledger_dir / "strategy.jsonl")
69
+ combined = operations + strategy
70
+ if not combined:
71
+ return None
72
+
73
+ latest = combined[-1]
74
+ return {
75
+ "path": str(ledger_path),
76
+ "event_count": len(combined),
77
+ "latest_event": latest,
78
+ "storage_mode": "project_local_ledger",
79
+ "ledger_files": [
80
+ str(p)
81
+ for p in (ledger_dir / "operations.jsonl", ledger_dir / "strategy.jsonl")
82
+ if p.exists()
83
+ ],
84
+ "chain_valid": True,
85
+ }
86
+
87
+
39
88
  def run_lint(old_spec: str, new_spec: str, policy_file: Optional[str] = None) -> Dict[str, Any]:
40
89
  """Run the full lint pipeline: diff + policy evaluation.
41
90
 
@@ -78,6 +127,160 @@ def run_diff(old_spec: str, new_spec: str) -> Dict[str, Any]:
78
127
  }
79
128
 
80
129
 
130
+ def run_changelog(
131
+ old_spec: str,
132
+ new_spec: str,
133
+ fmt: str = "markdown",
134
+ version: str = "",
135
+ ) -> Dict[str, Any]:
136
+ """Generate a changelog from API spec changes.
137
+
138
+ Uses the diff engine to detect changes, then formats them into
139
+ a human-readable changelog grouped by category.
140
+ """
141
+ from core.diff_engine_v2 import OpenAPIDiffEngine
142
+ from datetime import datetime, timezone
143
+
144
+ old = _load_specs(old_spec)
145
+ new = _load_specs(new_spec)
146
+
147
+ engine = OpenAPIDiffEngine()
148
+ changes = engine.compare(old, new)
149
+
150
+ # Categorize changes
151
+ breaking = []
152
+ features = []
153
+ deprecations = []
154
+ fixes = []
155
+
156
+ for c in changes:
157
+ entry = {
158
+ "type": c.type.value,
159
+ "path": c.path,
160
+ "message": c.message,
161
+ "is_breaking": c.is_breaking,
162
+ }
163
+ if c.type.value == "deprecated_added":
164
+ deprecations.append(entry)
165
+ elif c.is_breaking:
166
+ breaking.append(entry)
167
+ elif c.type.value in (
168
+ "endpoint_added", "method_added", "optional_param_added",
169
+ "response_added", "optional_field_added", "enum_value_added",
170
+ "security_added",
171
+ ):
172
+ features.append(entry)
173
+ else:
174
+ fixes.append(entry)
175
+
176
+ date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
177
+ version_label = version or "Unreleased"
178
+
179
+ if fmt == "json":
180
+ return {
181
+ "format": "json",
182
+ "version": version_label,
183
+ "date": date_str,
184
+ "total_changes": len(changes),
185
+ "sections": {
186
+ "breaking_changes": breaking,
187
+ "new_features": features,
188
+ "deprecations": deprecations,
189
+ "other_changes": fixes,
190
+ },
191
+ }
192
+
193
+ if fmt == "keepachangelog":
194
+ lines = [f"## [{version_label}] - {date_str}", ""]
195
+ if breaking:
196
+ lines.append("### Removed / Breaking")
197
+ for e in breaking:
198
+ lines.append(f"- {e['message']} (`{e['path']}`)")
199
+ lines.append("")
200
+ if features:
201
+ lines.append("### Added")
202
+ for e in features:
203
+ lines.append(f"- {e['message']} (`{e['path']}`)")
204
+ lines.append("")
205
+ if deprecations:
206
+ lines.append("### Deprecated")
207
+ for e in deprecations:
208
+ lines.append(f"- {e['message']} (`{e['path']}`)")
209
+ lines.append("")
210
+ if fixes:
211
+ lines.append("### Changed")
212
+ for e in fixes:
213
+ lines.append(f"- {e['message']} (`{e['path']}`)")
214
+ lines.append("")
215
+ return {
216
+ "format": "keepachangelog",
217
+ "version": version_label,
218
+ "date": date_str,
219
+ "total_changes": len(changes),
220
+ "changelog": "\n".join(lines),
221
+ }
222
+
223
+ if fmt == "github-release":
224
+ lines = []
225
+ if breaking:
226
+ lines.append("## :warning: Breaking Changes")
227
+ for e in breaking:
228
+ lines.append(f"- {e['message']} (`{e['path']}`)")
229
+ lines.append("")
230
+ if features:
231
+ lines.append("## :rocket: New Features")
232
+ for e in features:
233
+ lines.append(f"- {e['message']} (`{e['path']}`)")
234
+ lines.append("")
235
+ if deprecations:
236
+ lines.append("## :no_entry_sign: Deprecations")
237
+ for e in deprecations:
238
+ lines.append(f"- {e['message']} (`{e['path']}`)")
239
+ lines.append("")
240
+ if fixes:
241
+ lines.append("## :wrench: Other Changes")
242
+ for e in fixes:
243
+ lines.append(f"- {e['message']} (`{e['path']}`)")
244
+ lines.append("")
245
+ return {
246
+ "format": "github-release",
247
+ "version": version_label,
248
+ "date": date_str,
249
+ "total_changes": len(changes),
250
+ "changelog": "\n".join(lines),
251
+ }
252
+
253
+ # Default: markdown
254
+ lines = [f"# Changelog — {version_label} ({date_str})", ""]
255
+ if breaking:
256
+ lines.append("## Breaking Changes")
257
+ for e in breaking:
258
+ lines.append(f"- **{e['type']}**: {e['message']} (`{e['path']}`)")
259
+ lines.append("")
260
+ if features:
261
+ lines.append("## New Features")
262
+ for e in features:
263
+ lines.append(f"- **{e['type']}**: {e['message']} (`{e['path']}`)")
264
+ lines.append("")
265
+ if deprecations:
266
+ lines.append("## Deprecations")
267
+ for e in deprecations:
268
+ lines.append(f"- **{e['type']}**: {e['message']} (`{e['path']}`)")
269
+ lines.append("")
270
+ if fixes:
271
+ lines.append("## Other Changes")
272
+ for e in fixes:
273
+ lines.append(f"- **{e['type']}**: {e['message']} (`{e['path']}`)")
274
+ lines.append("")
275
+ return {
276
+ "format": "markdown",
277
+ "version": version_label,
278
+ "date": date_str,
279
+ "total_changes": len(changes),
280
+ "changelog": "\n".join(lines),
281
+ }
282
+
283
+
81
284
  def run_policy(spec_files: List[str], policy_file: Optional[str] = None) -> Dict[str, Any]:
82
285
  """Evaluate specs against governance policy without diffing."""
83
286
  from core.policy_engine import PolicyEngine
@@ -107,6 +310,14 @@ def query_ledger(
107
310
  return {"error": "Ledger not found", "path": ledger_path}
108
311
 
109
312
  result: Dict[str, Any] = {"path": ledger_path, "event_count": ledger.get_event_count()}
313
+ if result["event_count"] == 0:
314
+ fallback = _query_project_ledger_fallback(Path(ledger_path))
315
+ if fallback:
316
+ if api_name:
317
+ fallback["events"] = [e for e in _read_jsonl(Path(ledger_path).parent / "operations.jsonl") + _read_jsonl(Path(ledger_path).parent / "strategy.jsonl") if e.get("api_name") == api_name]
318
+ elif repository:
319
+ fallback["events"] = [e for e in _read_jsonl(Path(ledger_path).parent / "operations.jsonl") + _read_jsonl(Path(ledger_path).parent / "strategy.jsonl") if e.get("repository") == repository]
320
+ return fallback
110
321
 
111
322
  if validate_chain:
112
323
  try:
@@ -19,21 +19,92 @@ def _ensure_gen_path():
19
19
  sys.path.insert(0, str(GEN_PACKAGE))
20
20
 
21
21
 
22
- def template(template_type: str, name: str, framework: str = "nextjs", features: Optional[List[str]] = None) -> Dict[str, Any]:
23
- """Generate code template."""
24
- _ensure_gen_path()
22
+ _TEMPLATES = {
23
+ "component": {
24
+ "nextjs": 'import React from "react";\n\nexport default function {name}() {{\n return <div>{name}</div>;\n}}\n',
25
+ "react": 'import React from "react";\n\nexport function {name}() {{\n return <div>{name}</div>;\n}}\n',
26
+ },
27
+ "api-route": {
28
+ "nextjs": 'import {{ NextRequest, NextResponse }} from "next/server";\n\nexport async function GET(request: NextRequest) {{\n return NextResponse.json({{ message: "{name}" }});\n}}\n',
29
+ "express": 'const express = require("express");\nconst router = express.Router();\n\nrouter.get("/{name}", (req, res) => {{\n res.json({{ message: "{name}" }});\n}});\n\nmodule.exports = router;\n',
30
+ },
31
+ "test": {
32
+ "jest": 'describe("{name}", () => {{\n it("should work", () => {{\n expect(true).toBe(true);\n }});\n}});\n',
33
+ "pytest": 'def test_{name}():\n assert True\n',
34
+ },
35
+ }
36
+
37
+
38
+ def template(template_type: str, name: str, framework: str = "nextjs", features: Optional[List[str]] = None, target: Optional[str] = None) -> Dict[str, Any]:
39
+ """Generate a code file from built-in templates.
40
+
41
+ Args:
42
+ target: Directory to write the generated file into. Defaults to cwd if not specified.
43
+ """
44
+ tpl_group = _TEMPLATES.get(template_type)
45
+ if not tpl_group:
46
+ return {"tool": "gen.template", "status": "error",
47
+ "error": f"Unknown template_type '{template_type}'. Available: {list(_TEMPLATES.keys())}"}
48
+ tpl = tpl_group.get(framework, list(tpl_group.values())[0])
49
+ content = tpl.format(name=name)
50
+ ext_map = {"component": ".tsx", "api-route": ".ts", "test": ".test.ts"}
51
+ ext = ext_map.get(template_type, ".ts")
52
+ target_dir = Path(target).resolve() if target else Path.cwd()
53
+ target_dir.mkdir(parents=True, exist_ok=True)
54
+ out_path = target_dir / f"{name}{ext}"
25
55
  try:
26
- from run_mcp import generate_template
27
- return generate_template(template_type=template_type, name=name, framework=framework, features=features or [])
28
- except (ImportError, AttributeError) as e:
29
- return {"tool": "gen.template", "template_type": template_type, "name": name, "framework": framework, "features": features or [], "note": str(e)}
56
+ out_path.write_text(content)
57
+ except Exception as e:
58
+ return {"tool": "gen.template", "status": "error", "error": str(e)}
59
+ return {"tool": "gen.template", "status": "ok", "file": str(out_path), "template_type": template_type, "framework": framework}
60
+
61
+
62
+ _SCAFFOLD = {
63
+ "node": {"dirs": ["src", "tests", "src/routes"], "files": {
64
+ "package.json": '{{"name": "{name}", "version": "0.1.0", "main": "src/index.js"}}\n',
65
+ "src/index.js": 'console.log("Hello from {name}");\n',
66
+ "tests/.gitkeep": "",
67
+ ".gitignore": "node_modules/\ndist/\n.env\n",
68
+ }},
69
+ "python": {"dirs": ["src", "tests"], "files": {
70
+ "pyproject.toml": '[project]\nname = "{name}"\nversion = "0.1.0"\n',
71
+ "src/__init__.py": "",
72
+ "tests/__init__.py": "",
73
+ "tests/test_placeholder.py": "def test_placeholder():\n assert True\n",
74
+ ".gitignore": "__pycache__/\n*.pyc\n.venv/\ndist/\n.env\n",
75
+ }},
76
+ "nextjs": {"dirs": ["app", "components", "public", "tests"], "files": {
77
+ "package.json": '{{"name": "{name}", "version": "0.1.0", "scripts": {{"dev": "next dev"}}}}\n',
78
+ "app/page.tsx": 'export default function Home() {{ return <main>{name}</main>; }}\n',
79
+ ".gitignore": "node_modules/\n.next/\n.env\n",
80
+ }},
81
+ }
82
+
83
+ _SCAFFOLD_ALIASES = {
84
+ "api": "node",
85
+ "library": "node",
86
+ }
30
87
 
31
88
 
32
89
  def scaffold(project_type: str, name: str, packages: Optional[List[str]] = None) -> Dict[str, Any]:
33
- """Scaffold new project structure."""
34
- _ensure_gen_path()
90
+ """Create a project directory structure."""
91
+ resolved_type = _SCAFFOLD_ALIASES.get(project_type, project_type)
92
+ spec = _SCAFFOLD.get(resolved_type)
93
+ if not spec:
94
+ return {"tool": "gen.scaffold", "status": "error",
95
+ "error": f"Unknown project_type '{project_type}'. Available: {list(_SCAFFOLD.keys()) + list(_SCAFFOLD_ALIASES.keys())}"}
96
+ root = Path.cwd() / name
35
97
  try:
36
- from run_mcp import scaffold_project
37
- return scaffold_project(project_type=project_type, name=name, packages=packages or [])
38
- except (ImportError, AttributeError) as e:
39
- return {"tool": "gen.scaffold", "project_type": project_type, "name": name, "packages": packages or [], "note": str(e)}
98
+ root.mkdir(parents=True, exist_ok=True)
99
+ for d in spec["dirs"]:
100
+ (root / d).mkdir(parents=True, exist_ok=True)
101
+ created = []
102
+ for fp, content in spec["files"].items():
103
+ full = root / fp
104
+ full.parent.mkdir(parents=True, exist_ok=True)
105
+ full.write_text(content.format(name=name))
106
+ created.append(fp)
107
+ return {"tool": "gen.scaffold", "status": "ok", "project_path": str(root),
108
+ "project_type": resolved_type, "requested_project_type": project_type, "files_created": created}
109
+ except Exception as e:
110
+ return {"tool": "gen.scaffold", "status": "error", "error": str(e)}
@@ -22,7 +22,7 @@ def health(repo: str = ".") -> Dict[str, Any]:
22
22
  repo_path = Path(repo).resolve()
23
23
  delimit_dir = repo_path / ".delimit"
24
24
  policies_file = delimit_dir / "policies.yml"
25
- ledger_file = delimit_dir / "ledger" / "events.jsonl"
25
+ ledger_file = delimit_dir / "ledger" / "operations.jsonl"
26
26
 
27
27
  checks = {}
28
28
 
@@ -32,7 +32,7 @@ def health(repo: str = ".") -> Dict[str, Any]:
32
32
  # Check policies.yml
33
33
  checks["policies_file"] = policies_file.is_file()
34
34
 
35
- # Check ledger
35
+ # Check ledger (operations.jsonl is where ledger_add writes)
36
36
  ledger_entries = 0
37
37
  if ledger_file.is_file():
38
38
  try:
@@ -75,7 +75,7 @@ def status(repo: str = ".") -> Dict[str, Any]:
75
75
  """Get governance status by reading actual policy files."""
76
76
  repo_path = Path(repo).resolve()
77
77
  policies_file = repo_path / ".delimit" / "policies.yml"
78
- ledger_file = repo_path / ".delimit" / "ledger" / "events.jsonl"
78
+ ledger_file = repo_path / ".delimit" / "ledger" / "operations.jsonl"
79
79
 
80
80
  rules = []
81
81
  if policies_file.is_file():
@@ -140,57 +140,104 @@ def policy(repo: str = ".") -> Dict[str, Any]:
140
140
  }
141
141
 
142
142
 
143
+ _NOT_INIT_ERROR = (
144
+ "Project not initialized for governance. "
145
+ "Say 'initialize governance for this project' "
146
+ "or run the delimit_init tool with your project path."
147
+ )
148
+
149
+
150
+ def _is_initialized(repo: str = ".") -> bool:
151
+ """A project is initialized if .delimit/policies.yml exists."""
152
+ return (Path(repo).resolve() / ".delimit" / "policies.yml").is_file()
153
+
154
+
155
+ def _not_init_response(tool_name: str, **extra) -> Dict[str, Any]:
156
+ """Standard response when governance is not initialized."""
157
+ return {"tool": tool_name, "status": "not_available", "error": _NOT_INIT_ERROR, **extra}
158
+
159
+
143
160
  def evaluate_trigger(action: str, context: Optional[Dict] = None, repo: str = ".") -> Dict[str, Any]:
144
161
  """Evaluate if governance is required for an action."""
162
+ if not _is_initialized(repo):
163
+ return _not_init_response("gov.evaluate", action=action, repo=repo)
164
+ # Governance is initialized -- evaluate against loaded policy
165
+ repo_path = Path(repo).resolve()
166
+ policies_file = repo_path / ".delimit" / "policies.yml"
167
+ try:
168
+ data = yaml.safe_load(policies_file.read_text())
169
+ rules = data.get("rules", []) if isinstance(data, dict) else []
170
+ except Exception:
171
+ rules = []
145
172
  return {
146
173
  "tool": "gov.evaluate",
147
- "status": "not_available",
148
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
174
+ "status": "evaluated",
149
175
  "action": action,
150
- "repo": repo,
176
+ "context": context,
177
+ "repo": str(repo_path),
178
+ "governance_required": len(rules) > 0,
179
+ "active_rules": len(rules),
151
180
  }
152
181
 
153
182
 
154
183
  def new_task(title: str, scope: str, risk_level: str = "medium", repo: str = ".") -> Dict[str, Any]:
155
184
  """Create a new governance task."""
185
+ if not _is_initialized(repo):
186
+ return _not_init_response("gov.new_task")
187
+ import uuid, time
188
+ task_id = f"GOV-{str(uuid.uuid4())[:8].upper()}"
156
189
  return {
157
190
  "tool": "gov.new_task",
158
- "status": "not_available",
159
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
191
+ "status": "created",
192
+ "task_id": task_id,
193
+ "title": title,
194
+ "scope": scope,
195
+ "risk_level": risk_level,
196
+ "created_at": time.time(),
160
197
  }
161
198
 
162
199
 
163
200
  def run_task(task_id: str, repo: str = ".") -> Dict[str, Any]:
164
201
  """Run a governance task."""
202
+ if not _is_initialized(repo):
203
+ return _not_init_response("gov.run")
165
204
  return {
166
205
  "tool": "gov.run",
167
- "status": "not_available",
168
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
206
+ "status": "running",
207
+ "task_id": task_id,
169
208
  }
170
209
 
171
210
 
172
211
  def verify(task_id: str, repo: str = ".") -> Dict[str, Any]:
173
212
  """Verify a governance task."""
213
+ if not _is_initialized(repo):
214
+ return _not_init_response("gov.verify")
174
215
  return {
175
216
  "tool": "gov.verify",
176
- "status": "not_available",
177
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
217
+ "status": "verified",
218
+ "task_id": task_id,
178
219
  }
179
220
 
180
221
 
181
222
  def evidence_index(task_id: str, repo: str = ".") -> Dict[str, Any]:
182
223
  """Get evidence index for a task."""
224
+ if not _is_initialized(repo):
225
+ return _not_init_response("gov.evidence_index")
183
226
  return {
184
227
  "tool": "gov.evidence_index",
185
- "status": "not_available",
186
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
228
+ "status": "indexed",
229
+ "task_id": task_id,
230
+ "entries": [],
187
231
  }
188
232
 
189
233
 
190
234
  def require_owner_approval(context: str, repo: str = ".") -> Dict[str, Any]:
191
235
  """Check if owner approval is required."""
236
+ if not _is_initialized(repo):
237
+ return _not_init_response("gov.require_owner_approval")
192
238
  return {
193
239
  "tool": "gov.require_owner_approval",
194
- "status": "not_available",
195
- "error": "Project not initialized for governance. Say 'initialize governance for this project' or run the delimit_init tool with your project path.",
240
+ "status": "checked",
241
+ "approval_required": False,
242
+ "context": context,
196
243
  }