delimit-cli 3.14.28 → 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 (47) hide show
  1. package/gateway/ai/backends/deploy_bridge.py +56 -2
  2. package/gateway/ai/backends/gateway_core.py +212 -1
  3. package/gateway/ai/backends/generate_bridge.py +84 -13
  4. package/gateway/ai/backends/governance_bridge.py +63 -16
  5. package/gateway/ai/backends/memory_bridge.py +77 -76
  6. package/gateway/ai/backends/ops_bridge.py +76 -6
  7. package/gateway/ai/backends/os_bridge.py +23 -3
  8. package/gateway/ai/backends/repo_bridge.py +156 -17
  9. package/gateway/ai/backends/tools_design.py +116 -9
  10. package/gateway/ai/backends/tools_infra.py +200 -72
  11. package/gateway/ai/backends/tools_real.py +8 -0
  12. package/gateway/ai/backends/ui_bridge.py +115 -5
  13. package/gateway/ai/backends/vault_bridge.py +69 -114
  14. package/gateway/ai/content_engine.py +1276 -0
  15. package/gateway/ai/context_fs.py +193 -0
  16. package/gateway/ai/daemon.py +500 -0
  17. package/gateway/ai/data_plane.py +291 -0
  18. package/gateway/ai/deliberation.py +1033 -6
  19. package/gateway/ai/events.py +39 -0
  20. package/gateway/ai/founding_users.py +162 -0
  21. package/gateway/ai/governance.py +698 -4
  22. package/gateway/ai/inbox_daemon.py +78 -17
  23. package/gateway/ai/integrations/__init__.py +1 -0
  24. package/gateway/ai/integrations/opensage_wrapper.py +288 -0
  25. package/gateway/ai/key_resolver.py +95 -0
  26. package/gateway/ai/ledger_manager.py +289 -1
  27. package/gateway/ai/license.py +62 -4
  28. package/gateway/ai/license_core.py +208 -7
  29. package/gateway/ai/local_server.py +215 -0
  30. package/gateway/ai/loop_engine.py +408 -0
  31. package/gateway/ai/mcp_bridge.py +178 -0
  32. package/gateway/ai/release_sync.py +2 -2
  33. package/gateway/ai/screen_record.py +374 -0
  34. package/gateway/ai/secrets_broker.py +235 -0
  35. package/gateway/ai/social.py +189 -27
  36. package/gateway/ai/social_target.py +1635 -0
  37. package/gateway/ai/supabase_sync.py +190 -0
  38. package/gateway/ai/tracing.py +195 -0
  39. package/gateway/core/contract_ledger.py +1 -1
  40. package/gateway/core/dependency_graph.py +1 -1
  41. package/gateway/core/dependency_manifest.py +1 -1
  42. package/gateway/core/diff_engine_v2.py +272 -78
  43. package/gateway/core/event_backbone.py +2 -2
  44. package/gateway/core/event_schema.py +1 -1
  45. package/gateway/core/impact_analyzer.py +1 -1
  46. package/gateway/core/policy_engine.py +4 -0
  47. package/package.json +1 -1
@@ -1,4 +1,698 @@
1
- """Delimit Governance Layer — compiled binary required. Run: npx delimit-cli setup"""
2
- def govern(tool_name, result, project_path="."):
3
- result["next_steps"] = result.get("next_steps", [])
4
- return result
1
+ """
2
+ Delimit Governance Layer — the loop that keeps AI agents on track.
3
+
4
+ Every tool flows through governance. Governance:
5
+ 1. Logs what happened (evidence)
6
+ 2. Checks result against rules (thresholds, policies)
7
+ 3. Auto-creates ledger items for failures/warnings
8
+ 4. Suggests next steps (loops back to keep building)
9
+
10
+ This replaces _with_next_steps — governance IS the next step system.
11
+ """
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ def _is_test_mode() -> bool:
22
+ """Return True when DELIMIT_TEST_MODE is explicitly set.
23
+
24
+ When True the governance loop skips real ledger writes to avoid
25
+ polluting the project ledger with mock/test data.
26
+
27
+ We intentionally do NOT auto-detect PYTEST_CURRENT_TEST here
28
+ because the gateway's own test suite mocks ledger calls and needs
29
+ governance to attempt those calls so assertions work. Set
30
+ DELIMIT_TEST_MODE=1 in external test harnesses that trigger
31
+ governance but do not mock the ledger.
32
+ """
33
+ return bool(os.environ.get("DELIMIT_TEST_MODE"))
34
+
35
+ logger = logging.getLogger("delimit.governance")
36
+
37
+
38
+ def _ledger_list_items(project_path: str = ".") -> Dict[str, Any]:
39
+ """Indirection layer so tests can patch governance-local ledger hooks."""
40
+ import ai.ledger_manager as _lm
41
+ return _lm.list_items(project_path=project_path)
42
+
43
+
44
+ def _ledger_add_item(*, title: str, type: str, priority: str, source: str, project_path: str = ".") -> Dict[str, Any]:
45
+ """Indirection layer so tests can patch governance-local ledger hooks."""
46
+ import ai.ledger_manager as _lm
47
+ return _lm.add_item(
48
+ title=title,
49
+ type=type,
50
+ priority=priority,
51
+ source=source,
52
+ project_path=project_path,
53
+ )
54
+
55
+
56
+ def _ledger_update_item(item_id: str, *, status: str, project_path: str = ".") -> Dict[str, Any]:
57
+ """Indirection layer so tests can patch governance-local ledger hooks."""
58
+ import ai.ledger_manager as _lm
59
+ return _lm.update_item(item_id, status=status, project_path=project_path)
60
+
61
+
62
+ # Governance rules — what triggers auto-ledger-creation
63
+ RULES = {
64
+ "test_coverage": {
65
+ "threshold_key": "line_coverage",
66
+ "threshold": 80,
67
+ "comparison": "below",
68
+ "ledger_title": "Test coverage below {threshold}% — currently {value}%",
69
+ "ledger_type": "fix",
70
+ "ledger_priority": "P1",
71
+ },
72
+ "security_audit": {
73
+ "trigger_key": "vulnerabilities",
74
+ "trigger_if_nonempty": True,
75
+ "ledger_title": "Security: {count} vulnerabilities found",
76
+ "ledger_type": "fix",
77
+ "ledger_priority": "P0",
78
+ },
79
+ "security_scan": {
80
+ "trigger_key": "vulnerabilities",
81
+ "trigger_if_nonempty": True,
82
+ "ledger_title": "Security scan: {count} issues detected",
83
+ "ledger_type": "fix",
84
+ "ledger_priority": "P0",
85
+ },
86
+ "lint": {
87
+ "trigger_key": "violations",
88
+ "trigger_if_nonempty": True,
89
+ "ledger_title": "API lint: {count} violations found",
90
+ "ledger_type": "fix",
91
+ "ledger_priority": "P1",
92
+ },
93
+ "deliberate": {
94
+ "trigger_key": "unanimous",
95
+ "trigger_if_true": True,
96
+ "extract_actions": True,
97
+ "ledger_title": "Deliberation consensus reached — action items pending",
98
+ "ledger_type": "strategy",
99
+ "ledger_priority": "P1",
100
+ },
101
+ "gov_health": {
102
+ "trigger_key": "status",
103
+ "trigger_values": ["not_initialized", "degraded"],
104
+ "ledger_title": "Governance health: {value} — needs attention",
105
+ "ledger_type": "fix",
106
+ "ledger_priority": "P1",
107
+ },
108
+ "docs_validate": {
109
+ "threshold_key": "coverage_percent",
110
+ "threshold": 50,
111
+ "comparison": "below",
112
+ "ledger_title": "Documentation coverage below {threshold}% — currently {value}%",
113
+ "ledger_type": "task",
114
+ "ledger_priority": "P2",
115
+ },
116
+ }
117
+
118
+ # Milestone rules — auto-create DONE ledger items for significant completions.
119
+ # Unlike threshold RULES (which create open items for problems), milestones
120
+ # record achievements so the ledger reflects what was shipped.
121
+ MILESTONES = {
122
+ "deploy_site": {
123
+ "trigger_key": "status",
124
+ "trigger_values": ["deployed"],
125
+ "ledger_title": "Deployed: {project}",
126
+ "ledger_type": "feat",
127
+ "ledger_priority": "P1",
128
+ "auto_done": True,
129
+ },
130
+ "deploy_npm": {
131
+ "trigger_key": "status",
132
+ "trigger_values": ["published"],
133
+ "ledger_title": "Published: {package}@{new_version}",
134
+ "ledger_type": "feat",
135
+ "ledger_priority": "P1",
136
+ "auto_done": True,
137
+ },
138
+ "deliberate": {
139
+ "trigger_key": "status",
140
+ "trigger_values": ["unanimous"],
141
+ "ledger_title": "Consensus reached: {question_short}",
142
+ "ledger_type": "strategy",
143
+ "ledger_priority": "P1",
144
+ "auto_done": True,
145
+ },
146
+ "test_generate": {
147
+ "threshold_key": "tests_generated",
148
+ "threshold": 10,
149
+ "comparison": "above",
150
+ "ledger_title": "Generated {value} tests",
151
+ "ledger_type": "feat",
152
+ "ledger_priority": "P2",
153
+ "auto_done": True,
154
+ },
155
+ "sensor_github_issue": {
156
+ "trigger_key": "has_new_activity",
157
+ "trigger_if_true": True,
158
+ "ledger_title": "Outreach activity: {repo}#{issue_number}",
159
+ "ledger_type": "task",
160
+ "ledger_priority": "P1",
161
+ "auto_done": False, # needs follow-up
162
+ },
163
+ "zero_spec": {
164
+ "trigger_key": "success",
165
+ "trigger_if_true": True,
166
+ "ledger_title": "Zero-spec extracted: {framework} ({paths_count} paths)",
167
+ "ledger_type": "feat",
168
+ "ledger_priority": "P2",
169
+ "auto_done": True,
170
+ },
171
+ }
172
+
173
+ # Next steps registry — what to do after each tool
174
+ NEXT_STEPS = {
175
+ "lint": [
176
+ {"tool": "delimit_explain", "reason": "Get migration guide for violations", "premium": False},
177
+ {"tool": "delimit_semver", "reason": "Classify the version bump", "premium": False},
178
+ ],
179
+ "diff": [
180
+ {"tool": "delimit_semver", "reason": "Classify changes as MAJOR/MINOR/PATCH", "premium": False},
181
+ {"tool": "delimit_policy", "reason": "Check against governance policies", "premium": False},
182
+ ],
183
+ "semver": [
184
+ {"tool": "delimit_explain", "reason": "Generate human-readable changelog", "premium": False},
185
+ {"tool": "delimit_deploy_npm", "reason": "Publish the new version to npm", "premium": False},
186
+ ],
187
+ "init": [
188
+ {"tool": "delimit_gov_health", "reason": "Verify governance is set up correctly", "premium": True},
189
+ {"tool": "delimit_diagnose", "reason": "Check for any issues", "premium": False},
190
+ ],
191
+ "test_coverage": [
192
+ {"tool": "delimit_test_generate", "reason": "Generate tests for uncovered files", "premium": False},
193
+ ],
194
+ "security_audit": [
195
+ {"tool": "delimit_evidence_collect", "reason": "Collect evidence of findings", "premium": True},
196
+ ],
197
+ "gov_health": [
198
+ {"tool": "delimit_gov_status", "reason": "See detailed governance status", "premium": True},
199
+ {"tool": "delimit_repo_analyze", "reason": "Full repo health report", "premium": True},
200
+ ],
201
+ "deploy_npm": [
202
+ {"tool": "delimit_deploy_verify", "reason": "Verify the published package", "premium": True},
203
+ ],
204
+ "deploy_plan": [
205
+ {"tool": "delimit_deploy_build", "reason": "Build the deployment", "premium": True},
206
+ ],
207
+ "deploy_build": [
208
+ {"tool": "delimit_deploy_publish", "reason": "Publish the build", "premium": True},
209
+ ],
210
+ "deploy_publish": [
211
+ {"tool": "delimit_deploy_verify", "reason": "Verify the deployment", "premium": True},
212
+ ],
213
+ "deploy_verify": [
214
+ {"tool": "delimit_deploy_rollback", "reason": "Rollback if unhealthy", "premium": True},
215
+ ],
216
+ "repo_analyze": [
217
+ {"tool": "delimit_security_audit", "reason": "Scan for security issues", "premium": False},
218
+ {"tool": "delimit_gov_health", "reason": "Check governance status", "premium": True},
219
+ ],
220
+ "deliberate": [
221
+ {"tool": "delimit_ledger_context", "reason": "Review what's on the ledger after consensus", "premium": False},
222
+ ],
223
+ "ledger_add": [
224
+ {"tool": "delimit_ledger_context", "reason": "See updated ledger state", "premium": False},
225
+ ],
226
+ "diagnose": [
227
+ {"tool": "delimit_init", "reason": "Initialize governance if not set up", "premium": False},
228
+ ],
229
+ # Design & UI tools — triggered after UI-related work
230
+ "deploy_site": [
231
+ {"tool": "delimit_design_validate_responsive", "reason": "Check responsive design before deploy", "premium": False},
232
+ {"tool": "delimit_story_accessibility", "reason": "Run accessibility audit", "premium": False},
233
+ {"tool": "delimit_deploy_npm", "reason": "Publish npm package if applicable", "premium": False},
234
+ {"tool": "delimit_ledger_context", "reason": "Check what else needs deploying", "premium": False},
235
+ ],
236
+ "design_component_library": [
237
+ {"tool": "delimit_design_validate_responsive", "reason": "Validate responsive patterns", "premium": False},
238
+ {"tool": "delimit_story_accessibility", "reason": "Check accessibility", "premium": False},
239
+ ],
240
+ "design_validate_responsive": [
241
+ {"tool": "delimit_story_accessibility", "reason": "Also check accessibility", "premium": False},
242
+ {"tool": "delimit_story_visual_test", "reason": "Take visual baseline screenshot", "premium": False},
243
+ ],
244
+ "design_generate_component": [
245
+ {"tool": "delimit_story_generate", "reason": "Generate stories for the new component", "premium": False},
246
+ {"tool": "delimit_design_validate_responsive", "reason": "Check responsive design", "premium": False},
247
+ ],
248
+ "story_accessibility": [
249
+ {"tool": "delimit_ledger_add", "reason": "Track accessibility issues in ledger", "premium": False},
250
+ ],
251
+ "story_visual_test": [
252
+ {"tool": "delimit_ledger_add", "reason": "Track visual regressions in ledger", "premium": False},
253
+ ],
254
+ "scan": [
255
+ {"tool": "delimit_design_component_library", "reason": "Catalog UI components", "premium": False},
256
+ {"tool": "delimit_design_validate_responsive", "reason": "Check responsive design", "premium": False},
257
+ {"tool": "delimit_story_accessibility", "reason": "Run accessibility audit", "premium": False},
258
+ {"tool": "delimit_gov_health", "reason": "Check governance status", "premium": True},
259
+ ],
260
+ "quickstart": [
261
+ {"tool": "delimit_ledger_add", "reason": "Start tracking tasks in the ledger", "premium": False},
262
+ {"tool": "delimit_lint", "reason": "Check an OpenAPI spec for breaking changes", "premium": False},
263
+ {"tool": "delimit_deliberate", "reason": "Try multi-model deliberation on a decision", "premium": True},
264
+ {"tool": "delimit_security_scan", "reason": "Run a security scan", "premium": True},
265
+ ],
266
+ "test_generate": [
267
+ {"tool": "delimit_test_smoke", "reason": "Run smoke tests on generated tests", "premium": False},
268
+ {"tool": "delimit_test_coverage", "reason": "Check coverage after adding tests", "premium": False},
269
+ ],
270
+ # --- Context & Memory workflow ---
271
+ "context_init": [
272
+ {"tool": "delimit_context_write", "reason": "Write your first artifact", "premium": True},
273
+ ],
274
+ "context_write": [
275
+ {"tool": "delimit_context_list", "reason": "See all artifacts", "premium": True},
276
+ ],
277
+ "context_read": [], # Terminal — user got what they needed
278
+ "context_list": [], # Terminal
279
+ "context_snapshot": [
280
+ {"tool": "delimit_ledger_context", "reason": "Check what else needs work", "premium": False},
281
+ ],
282
+ "context_branch": [], # Terminal
283
+ "memory_store": [
284
+ {"tool": "delimit_ledger_context", "reason": "Check ledger after saving memory", "premium": False},
285
+ ],
286
+ "memory_search": [], # Terminal — user got results
287
+ "memory_recent": [], # Terminal
288
+ # --- Security workflow ---
289
+ "security_scan": [
290
+ {"tool": "delimit_security_audit", "reason": "Run full audit for details", "premium": False},
291
+ ],
292
+ "security_ingest": [
293
+ {"tool": "delimit_security_deliberate", "reason": "Triage findings via multi-model deliberation", "premium": True},
294
+ {"tool": "delimit_deploy_plan", "reason": "Check if deploys are gated by findings", "premium": True},
295
+ ],
296
+ "security_deliberate": [
297
+ {"tool": "delimit_ledger_context", "reason": "Review updated security findings in ledger", "premium": False},
298
+ ],
299
+ # --- Governance deep workflow ---
300
+ "gov_status": [
301
+ {"tool": "delimit_gov_evaluate", "reason": "Evaluate compliance", "premium": True},
302
+ ],
303
+ "gov_evaluate": [
304
+ {"tool": "delimit_gov_run", "reason": "Run governance checks", "premium": True},
305
+ ],
306
+ "gov_run": [
307
+ {"tool": "delimit_gov_verify", "reason": "Verify results", "premium": True},
308
+ ],
309
+ "gov_verify": [
310
+ {"tool": "delimit_ledger_context", "reason": "Check ledger for action items", "premium": False},
311
+ ],
312
+ "gov_policy": [], # Terminal
313
+ "gov_new_task": [
314
+ {"tool": "delimit_ledger_context", "reason": "See updated ledger", "premium": False},
315
+ ],
316
+ # --- Deploy workflow (missing entries) ---
317
+ "deploy_status": [
318
+ {"tool": "delimit_deploy_verify", "reason": "Verify health", "premium": True},
319
+ ],
320
+ "deploy_rollback": [
321
+ {"tool": "delimit_deploy_status", "reason": "Check rollback status", "premium": True},
322
+ ],
323
+ # --- Release workflow ---
324
+ "release_plan": [
325
+ {"tool": "delimit_release_validate", "reason": "Validate release readiness", "premium": True},
326
+ ],
327
+ "release_validate": [
328
+ {"tool": "delimit_release_sync", "reason": "Sync across surfaces", "premium": True},
329
+ ],
330
+ "release_sync": [
331
+ {"tool": "delimit_ledger_context", "reason": "Check for remaining items", "premium": False},
332
+ ],
333
+ "release_status": [], # Terminal
334
+ "release_history": [], # Terminal
335
+ "release_rollback": [
336
+ {"tool": "delimit_deploy_status", "reason": "Verify rollback", "premium": True},
337
+ ],
338
+ # --- Observability workflow ---
339
+ "obs_status": [
340
+ {"tool": "delimit_obs_metrics", "reason": "See detailed metrics", "premium": True},
341
+ ],
342
+ "obs_metrics": [
343
+ {"tool": "delimit_obs_logs", "reason": "Check logs for issues", "premium": True},
344
+ ],
345
+ "obs_logs": [
346
+ {"tool": "delimit_obs_alerts", "reason": "Check active alerts", "premium": True},
347
+ ],
348
+ "obs_alerts": [], # Terminal
349
+ # --- Repo workflow ---
350
+ "repo_diagnose": [
351
+ {"tool": "delimit_repo_analyze", "reason": "Full analysis", "premium": True},
352
+ ],
353
+ "repo_config_validate": [
354
+ {"tool": "delimit_repo_config_audit", "reason": "Audit for security issues", "premium": True},
355
+ ],
356
+ "repo_config_audit": [
357
+ {"tool": "delimit_security_audit", "reason": "Full security scan", "premium": False},
358
+ ],
359
+ # --- Docs workflow ---
360
+ "docs_generate": [
361
+ {"tool": "delimit_docs_validate", "reason": "Validate generated docs", "premium": False},
362
+ ],
363
+ "docs_validate": [
364
+ {"tool": "delimit_ledger_context", "reason": "Check for doc-related tasks", "premium": False},
365
+ ],
366
+ # --- Cost workflow ---
367
+ "cost_analyze": [
368
+ {"tool": "delimit_cost_optimize", "reason": "Find optimization opportunities", "premium": True},
369
+ ],
370
+ "cost_optimize": [
371
+ {"tool": "delimit_cost_alert", "reason": "Set cost alerts", "premium": True},
372
+ ],
373
+ "cost_alert": [], # Terminal
374
+ # --- Data workflow ---
375
+ "data_validate": [
376
+ {"tool": "delimit_data_backup", "reason": "Backup validated data", "premium": True},
377
+ ],
378
+ "data_backup": [], # Terminal
379
+ "data_migrate": [
380
+ {"tool": "delimit_data_validate", "reason": "Validate after migration", "premium": True},
381
+ ],
382
+ # --- Secrets workflow ---
383
+ "secret_store": [
384
+ {"tool": "delimit_secret_list", "reason": "Verify stored secrets", "premium": True},
385
+ ],
386
+ "secret_get": [], # Terminal
387
+ "secret_list": [], # Terminal
388
+ "secret_revoke": [], # Terminal
389
+ "secret_access_log": [], # Terminal
390
+ # --- Intel workflow ---
391
+ "intel_query": [], # Terminal
392
+ "intel_dataset_register": [
393
+ {"tool": "delimit_intel_dataset_list", "reason": "Verify registration", "premium": True},
394
+ ],
395
+ "intel_dataset_list": [], # Terminal
396
+ "intel_dataset_freeze": [], # Terminal
397
+ "intel_snapshot_ingest": [
398
+ {"tool": "delimit_intel_query", "reason": "Query the ingested data", "premium": True},
399
+ ],
400
+ # --- Social/Content workflow ---
401
+ "social_post": [
402
+ {"tool": "delimit_social_history", "reason": "Check post history", "premium": True},
403
+ ],
404
+ "social_generate": [
405
+ {"tool": "delimit_social_post", "reason": "Post the generated content", "premium": True},
406
+ ],
407
+ "social_history": [], # Terminal
408
+ "content_publish": [
409
+ {"tool": "delimit_content_schedule", "reason": "Check upcoming schedule", "premium": True},
410
+ ],
411
+ "content_schedule": [], # Terminal
412
+ "content_queue": [], # Terminal
413
+ # --- OS/Daemon workflow ---
414
+ "os_status": [
415
+ {"tool": "delimit_os_plan", "reason": "Plan next OS actions", "premium": True},
416
+ ],
417
+ "os_plan": [
418
+ {"tool": "delimit_os_gates", "reason": "Check gates", "premium": True},
419
+ ],
420
+ "os_gates": [], # Terminal
421
+ "daemon_status": [], # Terminal
422
+ "daemon_run": [
423
+ {"tool": "delimit_daemon_status", "reason": "Check results", "premium": True},
424
+ ],
425
+ "daemon_classify": [], # Terminal
426
+ # --- Resource/Vault/Misc ---
427
+ "resource_list": [], # Terminal
428
+ "resource_get": [], # Terminal
429
+ "resource_drivers": [], # Terminal
430
+ "vault_health": [], # Terminal
431
+ "vault_search": [], # Terminal
432
+ "vault_snapshot": [], # Terminal
433
+ "sensor_github_issue": [
434
+ {"tool": "delimit_ledger_context", "reason": "Check ledger for outreach items", "premium": False},
435
+ ],
436
+ "evidence_collect": [
437
+ {"tool": "delimit_evidence_verify", "reason": "Verify collected evidence", "premium": True},
438
+ ],
439
+ "evidence_verify": [], # Terminal
440
+ "generate_scaffold": [
441
+ {"tool": "delimit_init", "reason": "Initialize governance for the new project", "premium": False},
442
+ ],
443
+ "generate_template": [], # Terminal
444
+ # --- Terminal tools ---
445
+ "help": [],
446
+ "version": [],
447
+ "license_status": [],
448
+ "activate": [],
449
+ "ventures": [],
450
+ "ledger": [],
451
+ "ledger_list": [],
452
+ "ledger_done": [
453
+ {"tool": "delimit_ledger_context", "reason": "See what's next", "premium": False},
454
+ ],
455
+ "ledger_context": [], # Entry point — don't chain from it
456
+ "policy": [],
457
+ "explain": [],
458
+ "impact": [],
459
+ "zero_spec": [
460
+ {"tool": "delimit_lint", "reason": "Lint the extracted spec", "premium": False},
461
+ ],
462
+ "models": [],
463
+ "story_build": [],
464
+ "story_generate": [
465
+ {"tool": "delimit_story_accessibility", "reason": "Check accessibility", "premium": False},
466
+ ],
467
+ # --- Design extras (not yet routed) ---
468
+ "design_extract_tokens": [
469
+ {"tool": "delimit_design_generate_tailwind", "reason": "Generate Tailwind config from tokens", "premium": False},
470
+ ],
471
+ "design_generate_tailwind": [
472
+ {"tool": "delimit_design_validate_responsive", "reason": "Validate responsive design", "premium": False},
473
+ ],
474
+ # --- Test extras ---
475
+ "test_smoke": [
476
+ {"tool": "delimit_test_coverage", "reason": "Check coverage after smoke tests", "premium": False},
477
+ ],
478
+ }
479
+
480
+
481
+ def govern(tool_name: str, result: Dict[str, Any], project_path: str = ".") -> Dict[str, Any]:
482
+ """
483
+ Run governance on a tool's result. This is the central loop.
484
+
485
+ 1. Check result against rules
486
+ 2. Auto-create ledger items if thresholds breached
487
+ 3. Add next_steps for the AI to continue
488
+ 4. Return enriched result
489
+
490
+ Every tool should call this before returning.
491
+ """
492
+ # Strip "delimit_" prefix for rule matching
493
+ clean_name = tool_name.replace("delimit_", "")
494
+
495
+ governed_result = dict(result)
496
+
497
+ # 1. Check governance rules
498
+ rule = RULES.get(clean_name)
499
+ auto_items = []
500
+
501
+ if rule:
502
+ triggered = False
503
+ context = {}
504
+
505
+ # Threshold check (e.g., coverage < 80%)
506
+ if "threshold_key" in rule:
507
+ value = _deep_get(result, rule["threshold_key"])
508
+ if value is not None:
509
+ threshold = rule["threshold"]
510
+ if rule.get("comparison") == "below" and value < threshold:
511
+ triggered = True
512
+ context = {"value": f"{value:.1f}" if isinstance(value, float) else str(value), "threshold": str(threshold)}
513
+
514
+ # Non-empty list check (e.g., vulnerabilities found)
515
+ if "trigger_key" in rule and "trigger_if_nonempty" in rule:
516
+ items = _deep_get(result, rule["trigger_key"])
517
+ if items and isinstance(items, list) and len(items) > 0:
518
+ triggered = True
519
+ context = {"count": str(len(items))}
520
+
521
+ # Value match check (e.g., status == "degraded")
522
+ if "trigger_key" in rule and "trigger_values" in rule:
523
+ value = _deep_get(result, rule["trigger_key"])
524
+ if value in rule["trigger_values"]:
525
+ triggered = True
526
+ context = {"value": str(value)}
527
+
528
+ # Boolean check (e.g., unanimous == True)
529
+ if "trigger_key" in rule and "trigger_if_true" in rule:
530
+ value = _deep_get(result, rule["trigger_key"])
531
+ if value:
532
+ triggered = True
533
+
534
+ if triggered:
535
+ title = rule["ledger_title"].format(**context) if context else rule["ledger_title"]
536
+ auto_items.append({
537
+ "title": title,
538
+ "type": rule.get("ledger_type", "task"),
539
+ "priority": rule.get("ledger_priority", "P1"),
540
+ "source": f"governance:{clean_name}",
541
+ })
542
+
543
+ # 1b. Check milestone rules (auto-create DONE items for achievements)
544
+ milestone = MILESTONES.get(clean_name)
545
+ if milestone:
546
+ m_triggered = False
547
+ m_context = {}
548
+
549
+ # Value match (e.g., status == "deployed")
550
+ if "trigger_key" in milestone and "trigger_values" in milestone:
551
+ value = _deep_get(result, milestone["trigger_key"])
552
+ if value in milestone["trigger_values"]:
553
+ m_triggered = True
554
+ m_context = {"value": str(value)}
555
+
556
+ # Boolean check (e.g., success == True)
557
+ if "trigger_key" in milestone and milestone.get("trigger_if_true"):
558
+ value = _deep_get(result, milestone["trigger_key"])
559
+ if value:
560
+ m_triggered = True
561
+
562
+ # Threshold above (e.g., tests_generated > 10)
563
+ if "threshold_key" in milestone:
564
+ value = _deep_get(result, milestone["threshold_key"])
565
+ if value is not None:
566
+ threshold = milestone["threshold"]
567
+ if milestone.get("comparison") == "above" and value > threshold:
568
+ m_triggered = True
569
+ m_context = {"value": str(value), "threshold": str(threshold)}
570
+
571
+ if m_triggered:
572
+ # Build context from result fields for title interpolation
573
+ for key in ("project", "package", "new_version", "framework", "paths_count", "repo", "issue_number"):
574
+ if key not in m_context:
575
+ v = _deep_get(result, key)
576
+ if v is not None:
577
+ m_context[key] = str(v)
578
+ # Special: short question for deliberations
579
+ if "question_short" not in m_context:
580
+ q = _deep_get(result, "question") or _deep_get(result, "note") or ""
581
+ m_context["question_short"] = str(q)[:80]
582
+
583
+ try:
584
+ title = milestone["ledger_title"].format(**m_context)
585
+ except (KeyError, IndexError):
586
+ title = milestone["ledger_title"]
587
+
588
+ auto_items.append({
589
+ "title": title,
590
+ "type": milestone.get("ledger_type", "feat"),
591
+ "priority": milestone.get("ledger_priority", "P1"),
592
+ "source": f"milestone:{clean_name}",
593
+ "auto_done": milestone.get("auto_done", True),
594
+ })
595
+
596
+ # 2. Auto-create ledger items (with dedup — skip if open item with same title exists)
597
+ if auto_items:
598
+ try:
599
+ # Load existing open titles for dedup
600
+ existing = _ledger_list_items(project_path=project_path)
601
+ # items can be a list or dict of lists (by ledger type)
602
+ all_items = []
603
+ raw_items = existing.get("items", [])
604
+ if isinstance(raw_items, dict):
605
+ for ledger_items in raw_items.values():
606
+ if isinstance(ledger_items, list):
607
+ all_items.extend(ledger_items)
608
+ elif isinstance(raw_items, list):
609
+ all_items = raw_items
610
+ open_titles = {
611
+ i.get("title", "")
612
+ for i in all_items
613
+ if isinstance(i, dict) and i.get("status") == "open"
614
+ }
615
+ created = []
616
+ test_mode = _is_test_mode()
617
+ for item in auto_items:
618
+ if item["title"] in open_titles:
619
+ logger.debug("Skipping duplicate ledger item: %s", item["title"])
620
+ continue
621
+ if test_mode:
622
+ # In test mode, skip real ledger writes to avoid
623
+ # polluting the project ledger with mock/test data.
624
+ logger.debug("Test mode: skipping ledger write for %s", item["title"])
625
+ created.append(f"TEST-{item['title'][:40]}")
626
+ continue
627
+ entry = _ledger_add_item(
628
+ title=item["title"],
629
+ type=item["type"],
630
+ priority=item["priority"],
631
+ source=item["source"],
632
+ project_path=project_path,
633
+ )
634
+ item_id = entry.get("added", {}).get("id", "")
635
+ created.append(item_id)
636
+ # Auto-close milestone items
637
+ if item.get("auto_done") and item_id:
638
+ try:
639
+ _ = _ledger_update_item(item_id, status="done", project_path=project_path)
640
+ except Exception:
641
+ pass
642
+ governed_result["governance"] = {
643
+ "action": "ledger_items_created",
644
+ "items": created,
645
+ "reason": "Governance rule triggered by tool result",
646
+ }
647
+ except Exception as e:
648
+ logger.warning("Governance auto-ledger failed: %s", e)
649
+
650
+ # 3. Add governance-directed next steps
651
+ steps = NEXT_STEPS.get(clean_name, [])
652
+ if steps:
653
+ governed_result["next_steps"] = steps
654
+
655
+ # 4. GOVERNANCE LOOP: always route back to ledger_context
656
+ # This is not a suggestion — it's how the loop works.
657
+ # The AI should call ledger_context after every tool to check what's next.
658
+ # Ledger tools now route through governance for next_steps but skip auto-create
659
+ # (no rules/milestones defined for them, so no recursion risk)
660
+ SKIP_GOVERNANCE_LOOP = ("ventures", "version", "help", "diagnose", "activate", "license_status", "models", "scan")
661
+ if clean_name not in SKIP_GOVERNANCE_LOOP:
662
+ if "next_steps" not in governed_result:
663
+ governed_result["next_steps"] = []
664
+ # Don't suggest ledger_context to itself (circular)
665
+ if clean_name != "ledger_context":
666
+ existing = {s.get("tool") for s in governed_result.get("next_steps", [])}
667
+ if "delimit_ledger_context" not in existing:
668
+ governed_result["next_steps"].insert(0, {
669
+ "tool": "delimit_ledger_context",
670
+ "reason": "GOVERNANCE LOOP: check ledger for next action",
671
+ "premium": False,
672
+ "required": True,
673
+ })
674
+ else:
675
+ # Excluded tools still get the next_steps field (empty) for schema consistency
676
+ if "next_steps" not in governed_result:
677
+ governed_result["next_steps"] = []
678
+
679
+ return governed_result
680
+
681
+
682
+ def _deep_get(d: Dict, key: str) -> Any:
683
+ """Get a value from a dict, supporting nested keys with dots."""
684
+ if "." in key:
685
+ parts = key.split(".", 1)
686
+ sub = d.get(parts[0])
687
+ if isinstance(sub, dict):
688
+ return _deep_get(sub, parts[1])
689
+ return None
690
+
691
+ # Check top-level and common nested locations
692
+ if key in d:
693
+ return d[key]
694
+ # Check inside 'data', 'result', 'overall_coverage'
695
+ for wrapper in ["data", "result", "overall_coverage", "summary"]:
696
+ if isinstance(d.get(wrapper), dict) and key in d[wrapper]:
697
+ return d[wrapper][key]
698
+ return None