delimit-cli 4.1.43 → 4.1.44
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 +27 -0
- package/README.md +46 -5
- package/bin/delimit-cli.js +1523 -208
- package/bin/delimit-setup.js +8 -2
- package/gateway/ai/agent_dispatch.py +34 -2
- package/gateway/ai/backends/deploy_bridge.py +167 -12
- package/gateway/ai/content_engine.py +1276 -2
- package/gateway/ai/github_scanner.py +1 -1
- package/gateway/ai/governance.py +58 -0
- package/gateway/ai/key_resolver.py +95 -2
- package/gateway/ai/ledger_manager.py +13 -3
- package/gateway/ai/loop_engine.py +220 -349
- package/gateway/ai/notify.py +1786 -2
- package/gateway/ai/reddit_scanner.py +45 -1
- package/gateway/ai/screen_record.py +1 -1
- package/gateway/ai/secrets_broker.py +5 -1
- package/gateway/ai/social_cache.py +341 -0
- package/gateway/ai/social_daemon.py +41 -10
- package/gateway/ai/supabase_sync.py +190 -2
- package/gateway/ai/tui.py +594 -36
- package/gateway/core/zero_spec/express_extractor.py +2 -2
- package/gateway/core/zero_spec/nestjs_extractor.py +40 -9
- package/gateway/requirements.txt +3 -6
- package/package.json +4 -3
- package/scripts/demo-v420-clean.sh +267 -0
- package/scripts/demo-v420-deliberation.sh +217 -0
- package/scripts/demo-v420.sh +55 -0
- package/scripts/postinstall.js +4 -3
- package/scripts/publish-ci-guard.sh +30 -0
- package/scripts/record-and-upload.sh +132 -0
- package/scripts/release.sh +126 -0
- package/scripts/sync-gateway.sh +100 -0
- package/scripts/youtube-upload.py +141 -0
|
@@ -1,408 +1,279 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
"""Governed Executor for Continuous Build (LED-239).
|
|
2
|
+
|
|
3
|
+
Requirements (Consensus 123):
|
|
4
|
+
- root ledger in /root/.delimit is authoritative
|
|
5
|
+
- select only build-safe open items (feat, fix, task)
|
|
6
|
+
- resolve venture + repo before dispatch
|
|
7
|
+
- use Delimit swarm/governance as control plane
|
|
8
|
+
- every iteration must update ledger, audit trail, and session state
|
|
9
|
+
- no deploy/secrets/destructive actions without explicit gate
|
|
10
|
+
- enforce max-iteration, max-error, and max-cost safeguards
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
import json
|
|
14
|
+
import logging
|
|
15
|
+
from datetime import datetime, timezone
|
|
13
16
|
import os
|
|
14
17
|
import time
|
|
15
18
|
import uuid
|
|
16
19
|
from pathlib import Path
|
|
17
20
|
from typing import Any, Dict, List, Optional
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
SESSIONS_DIR = LOOP_DIR / "sessions"
|
|
21
|
-
|
|
22
|
-
# Actions the AI model must never auto-execute without human approval
|
|
23
|
-
DEFAULT_REQUIRE_APPROVAL = ["deploy", "social_post", "outreach", "publish"]
|
|
24
|
-
|
|
25
|
-
VALID_STATUSES = {"running", "paused", "stopped", "circuit_broken"}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def _ensure_dir():
|
|
29
|
-
"""Create the loop sessions directory if it doesn't exist."""
|
|
30
|
-
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
logger = logging.getLogger("delimit.ai.loop_engine")
|
|
31
23
|
|
|
24
|
+
# ── Configuration ────────────────────────────────────────────────────
|
|
25
|
+
ROOT_LEDGER_PATH = Path("/root/.delimit")
|
|
26
|
+
BUILD_SAFE_TYPES = ["feat", "fix", "task"]
|
|
27
|
+
MAX_ITERATIONS_DEFAULT = 10
|
|
28
|
+
MAX_COST_DEFAULT = 2.0
|
|
29
|
+
MAX_ERRORS_DEFAULT = 2
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return SESSIONS_DIR / f"{session_id}.json"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _load_session(session_id: str) -> Optional[Dict[str, Any]]:
|
|
39
|
-
"""Load session state from disk. Returns None if not found."""
|
|
40
|
-
path = _session_path(session_id)
|
|
41
|
-
if not path.exists():
|
|
42
|
-
return None
|
|
43
|
-
try:
|
|
44
|
-
return json.loads(path.read_text())
|
|
45
|
-
except (json.JSONDecodeError, OSError):
|
|
46
|
-
return None
|
|
31
|
+
# ── Session State ────────────────────────────────────────────────────
|
|
32
|
+
SESSION_DIR = Path.home() / ".delimit" / "loop" / "sessions"
|
|
47
33
|
|
|
34
|
+
def _ensure_session_dir():
|
|
35
|
+
SESSION_DIR.mkdir(parents=True, exist_ok=True)
|
|
48
36
|
|
|
49
37
|
def _save_session(session: Dict[str, Any]):
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
path = _session_path(session["session_id"])
|
|
38
|
+
_ensure_session_dir()
|
|
39
|
+
path = SESSION_DIR / f"{session['session_id']}.json"
|
|
53
40
|
path.write_text(json.dumps(session, indent=2))
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"""Create a new loop session with default safeguards."""
|
|
58
|
-
if not session_id:
|
|
59
|
-
session_id = str(uuid.uuid4())[:12]
|
|
42
|
+
def create_governed_session() -> Dict[str, Any]:
|
|
43
|
+
session_id = f"build-{uuid.uuid4().hex[:8]}"
|
|
60
44
|
session = {
|
|
61
45
|
"session_id": session_id,
|
|
62
|
-
"
|
|
46
|
+
"type": "governed_build",
|
|
47
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
63
48
|
"iterations": 0,
|
|
64
|
-
"max_iterations":
|
|
49
|
+
"max_iterations": MAX_ITERATIONS_DEFAULT,
|
|
65
50
|
"cost_incurred": 0.0,
|
|
66
|
-
"cost_cap":
|
|
51
|
+
"cost_cap": MAX_COST_DEFAULT,
|
|
67
52
|
"errors": 0,
|
|
68
|
-
"error_threshold":
|
|
53
|
+
"error_threshold": MAX_ERRORS_DEFAULT,
|
|
69
54
|
"tasks_completed": [],
|
|
70
|
-
"
|
|
71
|
-
"require_approval_for": list(DEFAULT_REQUIRE_APPROVAL),
|
|
72
|
-
"status": "running",
|
|
55
|
+
"status": "running"
|
|
73
56
|
}
|
|
74
57
|
_save_session(session)
|
|
75
58
|
return session
|
|
76
59
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if session["errors"] >= session["error_threshold"]:
|
|
125
|
-
session["status"] = "circuit_broken"
|
|
126
|
-
_save_session(session)
|
|
127
|
-
return {
|
|
128
|
-
"action": "STOP",
|
|
129
|
-
"reason": f"Circuit breaker: {session['errors']} errors hit threshold ({session['error_threshold']}).",
|
|
130
|
-
"safeguard": "circuit_breaker",
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _get_open_items(venture: str = "", project_path: str = ".") -> List[Dict[str, Any]]:
|
|
137
|
-
"""Query the ledger for open items, sorted by priority."""
|
|
60
|
+
# ── Venture & Repo Resolution ─────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def resolve_venture_context(venture_name: str) -> Dict[str, str]:
|
|
63
|
+
"""Resolve a venture name to its project path and repo URL."""
|
|
64
|
+
from ai.ledger_manager import list_ventures
|
|
65
|
+
|
|
66
|
+
ventures = list_ventures().get("ventures", {})
|
|
67
|
+
context = {"path": ".", "repo": "", "name": venture_name or "root"}
|
|
68
|
+
|
|
69
|
+
if not venture_name or venture_name == "root":
|
|
70
|
+
context["path"] = str(ROOT_LEDGER_PATH)
|
|
71
|
+
return context
|
|
72
|
+
|
|
73
|
+
if venture_name in ventures:
|
|
74
|
+
v = ventures[venture_name]
|
|
75
|
+
context["path"] = v.get("path", ".")
|
|
76
|
+
context["repo"] = v.get("repo", "")
|
|
77
|
+
return context
|
|
78
|
+
|
|
79
|
+
# Fallback to fuzzy match
|
|
80
|
+
for name, info in ventures.items():
|
|
81
|
+
if venture_name.lower() in name.lower():
|
|
82
|
+
context["path"] = info.get("path", ".")
|
|
83
|
+
context["repo"] = info.get("repo", "")
|
|
84
|
+
context["name"] = name
|
|
85
|
+
return context
|
|
86
|
+
|
|
87
|
+
return context
|
|
88
|
+
|
|
89
|
+
# ── Governed Selection ───────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def next_task(venture: str = "", max_risk: str = "", session_id: str = "") -> Dict[str, Any]:
|
|
92
|
+
"""Get the next task to work on. Wrapper for server.py compatibility."""
|
|
93
|
+
session = create_governed_session() if not session_id else {"session_id": session_id, "status": "running", "iterations": 0, "max_iterations": 50, "cost_incurred": 0, "cost_cap": 5, "errors": 0, "error_threshold": 3, "tasks_done": 0, "auto_consensus": False}
|
|
94
|
+
task = get_next_build_task(session)
|
|
95
|
+
if task is None:
|
|
96
|
+
from ai.ledger_manager import list_items
|
|
97
|
+
result = list_items(status="open", project_path=str(ROOT_LEDGER_PATH))
|
|
98
|
+
open_count = sum(len(v) for v in result.get("items", {}).values())
|
|
99
|
+
return {"action": "CONSENSUS", "reason": f"No build-safe items found ({open_count} open items, none actionable)", "remaining_items": open_count, "session": session}
|
|
100
|
+
return {"action": "BUILD", "task": task, "remaining_items": 0, "session": session}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_next_build_task(session: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
104
|
+
"""Select the next build-safe item from the authoritative root ledger."""
|
|
138
105
|
from ai.ledger_manager import list_items
|
|
139
|
-
|
|
106
|
+
|
|
107
|
+
# Authoritative root ledger check
|
|
108
|
+
result = list_items(status="open", project_path=str(ROOT_LEDGER_PATH))
|
|
140
109
|
items = []
|
|
141
110
|
for ledger_items in result.get("items", {}).values():
|
|
142
111
|
items.extend(ledger_items)
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
items.sort(key=lambda x: priority_order.get(x.get("priority", "P2"), 9))
|
|
147
|
-
return items
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _filter_actionable(items: List[Dict[str, Any]], max_risk: str = "") -> List[Dict[str, Any]]:
|
|
151
|
-
"""Filter out owner-only items and apply risk filtering.
|
|
152
|
-
|
|
153
|
-
Owner-only items are those with source='owner' or tags containing 'owner-action'.
|
|
154
|
-
"""
|
|
155
|
-
filtered = []
|
|
112
|
+
|
|
113
|
+
# Filter build-safe items only
|
|
114
|
+
actionable = []
|
|
156
115
|
for item in items:
|
|
157
|
-
|
|
158
|
-
tags = item.get("tags", [])
|
|
159
|
-
if "owner-action" in tags or "owner-only" in tags:
|
|
116
|
+
if item.get("type") not in BUILD_SAFE_TYPES:
|
|
160
117
|
continue
|
|
161
|
-
|
|
118
|
+
# Skip items that explicitly require owner action or are not for AI
|
|
119
|
+
tags = item.get("tags", [])
|
|
120
|
+
if "owner-action" in tags or "manual" in tags:
|
|
162
121
|
continue
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if max_risk:
|
|
166
|
-
item_risk = item.get("risk", "")
|
|
167
|
-
if item_risk and _risk_level(item_risk) > _risk_level(max_risk):
|
|
168
|
-
continue
|
|
169
|
-
|
|
170
|
-
filtered.append(item)
|
|
171
|
-
return filtered
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def _resolve_project_path(venture: str) -> str:
|
|
175
|
-
"""Resolve a venture name or path to a project directory path."""
|
|
176
|
-
if not venture:
|
|
177
|
-
return "."
|
|
178
|
-
# Direct path — use as-is
|
|
179
|
-
if venture.startswith("/") or venture.startswith("~"):
|
|
180
|
-
return str(Path(venture).expanduser())
|
|
181
|
-
if venture.startswith(".") or os.sep in venture:
|
|
182
|
-
return str(Path(venture).resolve())
|
|
183
|
-
# Try registered ventures
|
|
184
|
-
try:
|
|
185
|
-
from ai.ledger_manager import list_ventures
|
|
186
|
-
ventures = list_ventures()
|
|
187
|
-
for name, info in ventures.get("ventures", {}).items():
|
|
188
|
-
if name == venture or venture in name:
|
|
189
|
-
return info.get("path", ".")
|
|
190
|
-
except Exception:
|
|
191
|
-
pass
|
|
192
|
-
return "."
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
def _risk_level(risk: str) -> int:
|
|
196
|
-
"""Convert risk string to numeric level for comparison."""
|
|
197
|
-
levels = {"low": 1, "medium": 2, "high": 3, "critical": 4}
|
|
198
|
-
return levels.get(risk.lower(), 2)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def next_task(
|
|
202
|
-
venture: str = "",
|
|
203
|
-
max_risk: str = "",
|
|
204
|
-
session_id: str = "",
|
|
205
|
-
) -> Dict[str, Any]:
|
|
206
|
-
"""Get the next task to work on with safeguard checks.
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
Dict with action: BUILD (with task), CONSENSUS (generate new items), or STOP.
|
|
210
|
-
"""
|
|
211
|
-
session = _get_or_create_session(session_id)
|
|
212
|
-
|
|
213
|
-
# Check safeguards
|
|
214
|
-
stop = _check_safeguards(session)
|
|
215
|
-
if stop:
|
|
216
|
-
stop["session"] = _session_summary(session)
|
|
217
|
-
return stop
|
|
218
|
-
|
|
219
|
-
# Resolve venture path
|
|
220
|
-
project_path = _resolve_project_path(venture)
|
|
221
|
-
|
|
222
|
-
# Get open items
|
|
223
|
-
items = _get_open_items(venture=venture, project_path=project_path)
|
|
224
|
-
actionable = _filter_actionable(items, max_risk=max_risk)
|
|
225
|
-
|
|
122
|
+
actionable.append(item)
|
|
123
|
+
|
|
226
124
|
if not actionable:
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"action": "STOP",
|
|
235
|
-
"reason": "No actionable items in the ledger.",
|
|
236
|
-
"safeguard": "empty_ledger",
|
|
237
|
-
"session": _session_summary(session),
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
task = actionable[0]
|
|
241
|
-
|
|
242
|
-
# Check if this task requires approval
|
|
243
|
-
require_approval = session.get("require_approval_for", [])
|
|
244
|
-
task_tags = task.get("tags", [])
|
|
245
|
-
needs_approval = any(tag in require_approval for tag in task_tags)
|
|
246
|
-
task_type = task.get("type", "")
|
|
247
|
-
if task_type in require_approval:
|
|
248
|
-
needs_approval = True
|
|
249
|
-
|
|
250
|
-
result = {
|
|
251
|
-
"action": "BUILD",
|
|
252
|
-
"task": task,
|
|
253
|
-
"remaining_items": len(actionable) - 1,
|
|
254
|
-
"session": _session_summary(session),
|
|
255
|
-
}
|
|
256
|
-
if needs_approval:
|
|
257
|
-
result["approval_required"] = True
|
|
258
|
-
result["approval_reason"] = "Task type or tags match require_approval_for list."
|
|
259
|
-
|
|
260
|
-
return result
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def task_complete(
|
|
264
|
-
task_id: str,
|
|
265
|
-
result: str = "",
|
|
266
|
-
cost_incurred: float = 0.0,
|
|
267
|
-
error: str = "",
|
|
268
|
-
session_id: str = "",
|
|
269
|
-
venture: str = "",
|
|
270
|
-
) -> Dict[str, Any]:
|
|
271
|
-
"""Mark current task done and get the next one.
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Sort by priority
|
|
128
|
+
priority_map = {"P0": 0, "P1": 1, "P2": 2, "P3": 3}
|
|
129
|
+
actionable.sort(key=lambda x: priority_map.get(x.get("priority", "P2"), 9))
|
|
130
|
+
|
|
131
|
+
return actionable[0]
|
|
272
132
|
|
|
273
|
-
|
|
274
|
-
"""
|
|
275
|
-
session = _get_or_create_session(session_id)
|
|
133
|
+
# ── Swarm Dispatch & Execution ───────────────────────────────────────
|
|
276
134
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
135
|
+
def loop_config(session_id: str = "", max_iterations: int = 0,
|
|
136
|
+
cost_cap: float = 0.0, auto_consensus: bool = False,
|
|
137
|
+
error_threshold: int = 0, status: str = "",
|
|
138
|
+
require_approval_for: list = None) -> Dict[str, Any]:
|
|
139
|
+
"""Configure or create a loop session with safeguards."""
|
|
140
|
+
_ensure_session_dir()
|
|
280
141
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
142
|
+
# Load existing or create new
|
|
143
|
+
if session_id:
|
|
144
|
+
path = SESSION_DIR / f"{session_id}.json"
|
|
145
|
+
if path.exists():
|
|
146
|
+
session = json.loads(path.read_text())
|
|
147
|
+
else:
|
|
148
|
+
session = {
|
|
149
|
+
"session_id": session_id,
|
|
150
|
+
"type": "governed_build",
|
|
151
|
+
"started_at": datetime.now(timezone.utc).isoformat(),
|
|
152
|
+
"iterations": 0,
|
|
153
|
+
"max_iterations": max_iterations or MAX_ITERATIONS_DEFAULT,
|
|
154
|
+
"cost_incurred": 0.0,
|
|
155
|
+
"cost_cap": cost_cap or MAX_COST_DEFAULT,
|
|
156
|
+
"errors": 0,
|
|
157
|
+
"error_threshold": error_threshold or MAX_ERRORS_DEFAULT,
|
|
158
|
+
"tasks_completed": [],
|
|
159
|
+
"status": status or "running",
|
|
160
|
+
}
|
|
290
161
|
else:
|
|
291
|
-
session
|
|
292
|
-
"task_id": task_id,
|
|
293
|
-
"status": "done",
|
|
294
|
-
"result": result[:500] if result else "",
|
|
295
|
-
"completed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
296
|
-
"cost": cost_incurred,
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
_save_session(session)
|
|
300
|
-
|
|
301
|
-
# Mark ledger item as done (best-effort)
|
|
302
|
-
if not error:
|
|
303
|
-
try:
|
|
304
|
-
from ai.ledger_manager import update_item
|
|
305
|
-
project_path = _resolve_project_path(venture)
|
|
306
|
-
update_item(item_id=task_id, status="done", note=result[:200] if result else "Completed via build loop", project_path=project_path)
|
|
307
|
-
except Exception:
|
|
308
|
-
pass # Never let ledger sync break the loop
|
|
309
|
-
|
|
310
|
-
# Return the next task
|
|
311
|
-
return next_task(venture=venture, session_id=session["session_id"])
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def loop_status(session_id: str = "") -> Dict[str, Any]:
|
|
315
|
-
"""Return current session metrics."""
|
|
316
|
-
if not session_id:
|
|
317
|
-
# Try to find the most recent session
|
|
318
|
-
sessions = _list_sessions()
|
|
319
|
-
if not sessions:
|
|
320
|
-
return {"error": "No active loop sessions found."}
|
|
321
|
-
session_id = sessions[0]["session_id"]
|
|
322
|
-
|
|
323
|
-
session = _load_session(session_id)
|
|
324
|
-
if not session:
|
|
325
|
-
return {"error": f"Session {session_id} not found."}
|
|
162
|
+
session = create_governed_session()
|
|
326
163
|
|
|
327
|
-
|
|
328
|
-
"session": _session_summary(session),
|
|
329
|
-
"tasks_completed": session.get("tasks_completed", []),
|
|
330
|
-
"safeguards": {
|
|
331
|
-
"max_iterations": session["max_iterations"],
|
|
332
|
-
"cost_cap": session["cost_cap"],
|
|
333
|
-
"error_threshold": session["error_threshold"],
|
|
334
|
-
"require_approval_for": session.get("require_approval_for", []),
|
|
335
|
-
},
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
def loop_config(
|
|
340
|
-
session_id: str = "",
|
|
341
|
-
max_iterations: int = 0,
|
|
342
|
-
cost_cap: float = 0.0,
|
|
343
|
-
auto_consensus: Optional[bool] = None,
|
|
344
|
-
error_threshold: int = 0,
|
|
345
|
-
status: str = "",
|
|
346
|
-
require_approval_for: Optional[List[str]] = None,
|
|
347
|
-
) -> Dict[str, Any]:
|
|
348
|
-
"""Update session configuration. Only provided values are changed."""
|
|
349
|
-
session = _get_or_create_session(session_id)
|
|
350
|
-
|
|
351
|
-
changes = {}
|
|
164
|
+
# Apply non-zero/non-empty overrides
|
|
352
165
|
if max_iterations > 0:
|
|
353
166
|
session["max_iterations"] = max_iterations
|
|
354
|
-
changes["max_iterations"] = max_iterations
|
|
355
167
|
if cost_cap > 0:
|
|
356
168
|
session["cost_cap"] = cost_cap
|
|
357
|
-
changes["cost_cap"] = cost_cap
|
|
358
|
-
if auto_consensus is not None:
|
|
359
|
-
session["auto_consensus"] = auto_consensus
|
|
360
|
-
changes["auto_consensus"] = auto_consensus
|
|
361
169
|
if error_threshold > 0:
|
|
362
170
|
session["error_threshold"] = error_threshold
|
|
363
|
-
|
|
364
|
-
if status and status in VALID_STATUSES:
|
|
171
|
+
if status:
|
|
365
172
|
session["status"] = status
|
|
366
|
-
|
|
173
|
+
if auto_consensus:
|
|
174
|
+
session["auto_consensus"] = True
|
|
367
175
|
if require_approval_for is not None:
|
|
368
176
|
session["require_approval_for"] = require_approval_for
|
|
369
|
-
changes["require_approval_for"] = require_approval_for
|
|
370
177
|
|
|
371
178
|
_save_session(session)
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
"session_id": session["session_id"],
|
|
375
|
-
"changes": changes,
|
|
376
|
-
"current_config": _session_summary(session),
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
def _session_summary(session: Dict[str, Any]) -> Dict[str, Any]:
|
|
381
|
-
"""Return a concise session summary for inclusion in responses."""
|
|
382
179
|
return {
|
|
383
180
|
"session_id": session["session_id"],
|
|
384
181
|
"status": session["status"],
|
|
385
|
-
"iterations": session["iterations"],
|
|
386
182
|
"max_iterations": session["max_iterations"],
|
|
387
|
-
"
|
|
183
|
+
"iterations": session.get("iterations", 0),
|
|
388
184
|
"cost_cap": session["cost_cap"],
|
|
389
|
-
"
|
|
185
|
+
"cost_incurred": session.get("cost_incurred", 0.0),
|
|
390
186
|
"error_threshold": session["error_threshold"],
|
|
391
|
-
"
|
|
392
|
-
"auto_consensus": session.get("auto_consensus", False),
|
|
187
|
+
"errors": session.get("errors", 0),
|
|
393
188
|
}
|
|
394
189
|
|
|
395
190
|
|
|
396
|
-
def
|
|
397
|
-
"""
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
191
|
+
def run_governed_iteration(session_id: str) -> Dict[str, Any]:
|
|
192
|
+
"""Execute one governed build iteration."""
|
|
193
|
+
from datetime import datetime, timezone
|
|
194
|
+
from ai.swarm import dispatch_task
|
|
195
|
+
|
|
196
|
+
# 1. Load Session & Check Safeguards
|
|
197
|
+
path = SESSION_DIR / f"{session_id}.json"
|
|
198
|
+
if not path.exists():
|
|
199
|
+
return {"error": f"Session {session_id} not found"}
|
|
200
|
+
session = json.loads(path.read_text())
|
|
201
|
+
|
|
202
|
+
if session["status"] != "running":
|
|
203
|
+
return {"status": "stopped", "reason": f"Session status is {session['status']}"}
|
|
204
|
+
|
|
205
|
+
if session["iterations"] >= session["max_iterations"]:
|
|
206
|
+
session["status"] = "finished"
|
|
207
|
+
_save_session(session)
|
|
208
|
+
return {"status": "finished", "reason": "Max iterations reached"}
|
|
209
|
+
|
|
210
|
+
if session["cost_incurred"] >= session["cost_cap"]:
|
|
211
|
+
session["status"] = "stopped"
|
|
212
|
+
_save_session(session)
|
|
213
|
+
return {"status": "stopped", "reason": "Cost cap reached"}
|
|
214
|
+
|
|
215
|
+
# 2. Select Task
|
|
216
|
+
task = get_next_build_task(session)
|
|
217
|
+
if not task:
|
|
218
|
+
return {"status": "idle", "reason": "No build-safe items in ledger"}
|
|
219
|
+
|
|
220
|
+
# 3. Resolve Context
|
|
221
|
+
v_name = task.get("venture", "root")
|
|
222
|
+
ctx = resolve_venture_context(v_name)
|
|
223
|
+
|
|
224
|
+
# 4. Dispatch through Swarm (Control Plane)
|
|
225
|
+
logger.info(f"Dispatching build task {task['id']} for venture {v_name}")
|
|
226
|
+
|
|
227
|
+
start_time = time.time()
|
|
228
|
+
try:
|
|
229
|
+
# Note: Swarm dispatch is the central point of governance
|
|
230
|
+
dispatch_result = dispatch_task(
|
|
231
|
+
title=task["title"],
|
|
232
|
+
description=task["description"],
|
|
233
|
+
context=f"Executing governed build loop for {v_name}. Ledger ID: {task['id']}",
|
|
234
|
+
project_path=ctx["path"],
|
|
235
|
+
priority=task["priority"]
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# 5. Update State & Ledger
|
|
239
|
+
duration = time.time() - start_time
|
|
240
|
+
cost = dispatch_result.get("estimated_cost", 0.05) # Default placeholder if missing
|
|
241
|
+
|
|
242
|
+
session["iterations"] += 1
|
|
243
|
+
session["cost_incurred"] += cost
|
|
244
|
+
|
|
245
|
+
from ai.ledger_manager import update_item
|
|
246
|
+
if dispatch_result.get("status") == "completed":
|
|
247
|
+
update_item(
|
|
248
|
+
item_id=task["id"],
|
|
249
|
+
status="done",
|
|
250
|
+
note=f"Completed via governed build loop. Result: {dispatch_result.get('summary', 'OK')}",
|
|
251
|
+
project_path=str(ROOT_LEDGER_PATH)
|
|
252
|
+
)
|
|
253
|
+
session["tasks_completed"].append({
|
|
254
|
+
"id": task["id"],
|
|
255
|
+
"status": "success",
|
|
256
|
+
"duration": duration,
|
|
257
|
+
"cost": cost
|
|
258
|
+
})
|
|
259
|
+
else:
|
|
260
|
+
session["errors"] += 1
|
|
261
|
+
if session["errors"] >= session["error_threshold"]:
|
|
262
|
+
session["status"] = "circuit_broken"
|
|
263
|
+
session["tasks_completed"].append({
|
|
264
|
+
"id": task["id"],
|
|
265
|
+
"status": "failed",
|
|
266
|
+
"error": dispatch_result.get("error", "Dispatch failed")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
_save_session(session)
|
|
270
|
+
return {"status": "continued", "task_id": task["id"], "result": dispatch_result}
|
|
271
|
+
|
|
272
|
+
except Exception as e:
|
|
273
|
+
session["errors"] += 1
|
|
274
|
+
_save_session(session)
|
|
275
|
+
return {"error": str(e)}
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
# Test pass if run directly
|
|
279
|
+
pass
|