delimit-cli 3.14.28 → 3.14.30
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/bin/delimit-setup.js +3 -6
- package/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
package/bin/delimit-setup.js
CHANGED
|
@@ -81,12 +81,9 @@ async function main() {
|
|
|
81
81
|
if (latest && latest !== _pkg.version && latest > _pkg.version) {
|
|
82
82
|
log(dim(` Updating delimit-cli ${_pkg.version} -> ${latest}...`));
|
|
83
83
|
execSync('npm install -g delimit-cli@latest 2>/dev/null', { stdio: 'pipe', timeout: 30000 });
|
|
84
|
-
// Re-exec with the updated version
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
execSync(`node "${setupPath}"`, { stdio: 'inherit' });
|
|
88
|
-
process.exit(0);
|
|
89
|
-
}
|
|
84
|
+
// Re-exec with the updated version from the NEW install location
|
|
85
|
+
execSync('delimit-cli setup', { stdio: 'inherit' });
|
|
86
|
+
process.exit(0);
|
|
90
87
|
}
|
|
91
88
|
} catch { /* offline or timeout — continue with current version */ }
|
|
92
89
|
|
|
@@ -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
|
-
|
|
23
|
-
""
|
|
24
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
"""
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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" / "
|
|
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" / "
|
|
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": "
|
|
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
|
-
"
|
|
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": "
|
|
159
|
-
"
|
|
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": "
|
|
168
|
-
"
|
|
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": "
|
|
177
|
-
"
|
|
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": "
|
|
186
|
-
"
|
|
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": "
|
|
195
|
-
"
|
|
240
|
+
"status": "checked",
|
|
241
|
+
"approval_required": False,
|
|
242
|
+
"context": context,
|
|
196
243
|
}
|