devcouncil 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import secrets
5
+ import time
6
+ from importlib import resources
7
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+
11
+ from devcouncil.app.project_status import compute_phase
12
+ from devcouncil.integrations.actions import apply_integration_target
13
+ from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
14
+ from devcouncil.storage.db import get_db
15
+ from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository, TaskRepository
16
+ from devcouncil.telemetry.traces import read_trace_events
17
+
18
+ LOGO_ASSET = "devcouncil_logo_premium.png"
19
+ LEGACY_LOGO_ASSET = "devcouncil-logo.svg"
20
+
21
+
22
+ # mtime-keyed cache so the dashboard's poll loop doesn't re-read and re-parse
23
+ # every run manifest on each refresh.
24
+ _RUN_MANIFEST_CACHE: dict[str, tuple[float, dict]] = {}
25
+
26
+
27
+ def _load_run_manifest(manifest_path: Path) -> dict | None:
28
+ key = str(manifest_path)
29
+ try:
30
+ mtime = manifest_path.stat().st_mtime
31
+ except OSError:
32
+ return None
33
+ cached = _RUN_MANIFEST_CACHE.get(key)
34
+ if cached is not None and cached[0] == mtime:
35
+ return dict(cached[1])
36
+ try:
37
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8")) or {}
38
+ except (json.JSONDecodeError, OSError):
39
+ return None
40
+ _RUN_MANIFEST_CACHE[key] = (mtime, manifest)
41
+ return dict(manifest)
42
+
43
+
44
+ def recent_run_artifacts(project_root: Path, *, limit: int = 10) -> list[dict]:
45
+ runs_dir = project_root / ".devcouncil" / "runs"
46
+ if not runs_dir.exists():
47
+ return []
48
+ manifests: list[dict] = []
49
+ for manifest_path in sorted(
50
+ runs_dir.glob("*/agent-run.json"),
51
+ key=lambda path: path.stat().st_mtime,
52
+ reverse=True,
53
+ ):
54
+ manifest = _load_run_manifest(manifest_path)
55
+ if manifest is None:
56
+ continue
57
+ manifest["manifest_path"] = str(manifest_path)
58
+ manifests.append(manifest)
59
+ if len(manifests) >= limit:
60
+ break
61
+ return manifests
62
+
63
+
64
+ # Integration status probes the filesystem (and optionally CLIs) — far too
65
+ # expensive to recompute on every 2-second dashboard poll.
66
+ _INTEGRATION_SUMMARY_TTL_SECONDS = 5.0
67
+ _INTEGRATION_SUMMARY_CACHE: dict[str, tuple[float, dict]] = {}
68
+
69
+
70
+ def _integration_summary_cached(project_root: Path) -> dict:
71
+ key = str(project_root)
72
+ now = time.monotonic()
73
+ cached = _INTEGRATION_SUMMARY_CACHE.get(key)
74
+ if cached is not None and now - cached[0] < _INTEGRATION_SUMMARY_TTL_SECONDS:
75
+ return cached[1]
76
+ summary = integration_status_summary(project_root)
77
+ _INTEGRATION_SUMMARY_CACHE[key] = (now, summary)
78
+ return summary
79
+
80
+
81
+ def _invalidate_integration_summary(project_root: Path) -> None:
82
+ _INTEGRATION_SUMMARY_CACHE.pop(str(project_root), None)
83
+
84
+
85
+ def logo_svg() -> str:
86
+ return resources.files("devcouncil.assets").joinpath(LEGACY_LOGO_ASSET).read_text(encoding="utf-8")
87
+
88
+
89
+ def logo_asset_bytes() -> bytes:
90
+ return resources.files("devcouncil.assets").joinpath(LOGO_ASSET).read_bytes()
91
+
92
+
93
+ def dashboard_payload(project_root: Path) -> dict:
94
+ db = get_db(project_root)
95
+ if not db:
96
+ return {
97
+ "initialized": False,
98
+ "phase": "UNINITIALIZED",
99
+ "tasks": [],
100
+ "coverage": {},
101
+ "events": [],
102
+ "integrations": _integration_summary_cached(project_root),
103
+ "recent_runs": recent_run_artifacts(project_root),
104
+ }
105
+ with db.get_session() as session:
106
+ graph = ArtifactGraphRepository(session).load_graph()
107
+ state = StateRepository(session).get_state()
108
+ phase = compute_phase(graph, state.current_phase if state else None)
109
+ tasks = [task.model_dump() for task in TaskRepository(session).get_all()]
110
+ return {
111
+ "initialized": True,
112
+ "phase": phase,
113
+ "coverage": graph.coverage_summary(),
114
+ "tasks": tasks,
115
+ "events": [event.model_dump(by_alias=True) for event in list(read_trace_events(project_root))[-50:]],
116
+ "integrations": _integration_summary_cached(project_root),
117
+ "recent_runs": recent_run_artifacts(project_root),
118
+ }
119
+
120
+
121
+ def _json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict) -> None:
122
+ body = json.dumps(payload).encode("utf-8")
123
+ handler.send_response(status)
124
+ handler.send_header("Content-Type", "application/json")
125
+ handler.send_header("Content-Length", str(len(body)))
126
+ handler.end_headers()
127
+ handler.wfile.write(body)
128
+
129
+
130
+ def _is_loopback_client(handler: BaseHTTPRequestHandler) -> bool:
131
+ host = handler.client_address[0] if handler.client_address else ""
132
+ return host in {"127.0.0.1", "::1", "localhost"}
133
+
134
+
135
+ def dashboard_apply_payload(
136
+ project_root: Path,
137
+ raw_body: bytes,
138
+ *,
139
+ token: str,
140
+ provided_token: str | None,
141
+ ) -> dict:
142
+ if not token or provided_token != token:
143
+ return {"ok": False, "error": "invalid dashboard token"}
144
+ try:
145
+ request = json.loads(raw_body.decode("utf-8") or "{}")
146
+ except json.JSONDecodeError:
147
+ return {"ok": False, "error": "invalid JSON body"}
148
+ if not isinstance(request, dict):
149
+ return {"ok": False, "error": "request body must be a JSON object"}
150
+ target = str(request.get("target") or "").strip()
151
+ include_hooks = bool(request.get("include_hooks", True))
152
+ strict = bool(request.get("strict", False))
153
+ try:
154
+ report = apply_integration_target(
155
+ project_root,
156
+ target,
157
+ include_hooks=include_hooks,
158
+ strict=strict,
159
+ )
160
+ except ValueError as exc:
161
+ return {"ok": False, "error": str(exc)}
162
+ # The next poll should reflect the freshly applied integration.
163
+ _invalidate_integration_summary(project_root)
164
+ return report.as_dict()
165
+
166
+
167
+ def dashboard_html(token: str = "") -> str:
168
+ safe_token = token.replace('"', "")
169
+ return f"""<!doctype html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="utf-8">
173
+ <meta name="viewport" content="width=device-width, initial-scale=1">
174
+ <meta name="devcouncil-dashboard-token" content="{safe_token}">
175
+ <title>DevCouncil Dashboard</title>
176
+ <style>
177
+ body {{ margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f7f7f5; color: #202124; }}
178
+ header {{ padding: 18px 28px; border-bottom: 1px solid #d9d9d4; background: #ffffff; display: flex; align-items: center; justify-content: space-between; gap: 18px; }}
179
+ main {{ padding: 24px 28px; display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }}
180
+ section {{ background: #ffffff; border: 1px solid #d9d9d4; border-radius: 8px; padding: 16px; }}
181
+ h1 {{ font-size: 20px; margin: 0; }}
182
+ h2 {{ font-size: 15px; margin: 0 0 12px; }}
183
+ .brand {{ display: flex; align-items: center; gap: 12px; min-width: 0; }}
184
+ .brand img {{ width: 54px; height: 54px; flex: 0 0 auto; filter: drop-shadow(0 8px 12px rgba(12, 36, 52, 0.22)); }}
185
+ .phase-line {{ white-space: nowrap; }}
186
+ .phase {{ font-weight: 700; }}
187
+ table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
188
+ th, td {{ text-align: left; border-bottom: 1px solid #ededeb; padding: 8px; vertical-align: top; }}
189
+ pre {{ margin: 0; white-space: pre-wrap; font-size: 12px; }}
190
+ .toolbar {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }}
191
+ button {{ border: 1px solid #b9b9b2; background: #ffffff; color: #202124; border-radius: 6px; padding: 6px 10px; font: inherit; cursor: pointer; }}
192
+ button:disabled {{ color: #898984; cursor: default; background: #f3f3f0; }}
193
+ .status-pill {{ display: inline-block; min-width: 58px; padding: 2px 6px; border-radius: 999px; font-size: 12px; text-align: center; background: #ededeb; }}
194
+ .status-ok {{ background: #dff3e4; color: #155724; }}
195
+ .status-warn {{ background: #fff3cd; color: #664d03; }}
196
+ .status-fail {{ background: #f8d7da; color: #842029; }}
197
+ #integration-result {{ margin-top: 10px; font-size: 12px; }}
198
+ @media (max-width: 800px) {{ main {{ grid-template-columns: 1fr; padding: 16px; }} header {{ padding: 16px; }} }}
199
+ </style>
200
+ </head>
201
+ <body>
202
+ <header><div class="brand"><img src="/assets/devcouncil_logo_premium.png" alt="DevCouncil logo"><h1>DevCouncil Dashboard</h1></div><div class="phase-line">Phase: <span id="phase" class="phase">loading</span></div></header>
203
+ <main>
204
+ <section><h2>Coverage</h2><pre id="coverage">{{}}</pre></section>
205
+ <section><h2>Tasks</h2><table><thead><tr><th>ID</th><th>Status</th><th>Title</th></tr></thead><tbody id="tasks"></tbody></table></section>
206
+ <section style="grid-column: 1 / -1;">
207
+ <h2>CLI Integrations</h2>
208
+ <div class="toolbar">
209
+ <button type="button" data-target="all">Apply Detected</button>
210
+ <button type="button" data-target="hooks">Install Hooks</button>
211
+ <button type="button" id="run-check">Run Check</button>
212
+ </div>
213
+ <pre id="integration-result"></pre>
214
+ <table>
215
+ <thead><tr><th>Client</th><th>PATH</th><th>MCP</th><th>Hooks</th><th>Launcher</th><th>Configured</th><th>Notes</th><th>Action</th></tr></thead>
216
+ <tbody id="integrations"></tbody>
217
+ </table>
218
+ </section>
219
+ <section style="grid-column: 1 / -1;">
220
+ <h2>Recent Agent Runs</h2>
221
+ <table>
222
+ <thead><tr><th>Run</th><th>Task</th><th>Agent</th><th>Status</th><th>Transcript</th></tr></thead>
223
+ <tbody id="runs"></tbody>
224
+ </table>
225
+ </section>
226
+ <section style="grid-column: 1 / -1;"><h2>Recent Trace Events</h2><pre id="events"></pre></section>
227
+ </main>
228
+ <script>
229
+ function setText(cell, value) {{
230
+ cell.textContent = value == null ? '' : String(value);
231
+ return cell;
232
+ }}
233
+
234
+ function statusClass(value) {{
235
+ if (value === 'ok') return 'status-pill status-ok';
236
+ if (value === 'missing' || value === 'drifted') return 'status-pill status-warn';
237
+ return 'status-pill';
238
+ }}
239
+
240
+ function statusCell(value) {{
241
+ const cell = document.createElement('td');
242
+ const pill = document.createElement('span');
243
+ pill.className = statusClass(value);
244
+ pill.textContent = value || 'n/a';
245
+ cell.appendChild(pill);
246
+ return cell;
247
+ }}
248
+
249
+ const token = document.querySelector('meta[name="devcouncil-dashboard-token"]').content;
250
+
251
+ async function applyIntegration(target) {{
252
+ const resultBox = document.getElementById('integration-result');
253
+ resultBox.textContent = `Running ${{target}}...`;
254
+ const res = await fetch('/api/integrations/apply', {{
255
+ method: 'POST',
256
+ headers: {{
257
+ 'Content-Type': 'application/json',
258
+ 'X-DevCouncil-Dashboard-Token': token,
259
+ }},
260
+ body: JSON.stringify({{ target }}),
261
+ }});
262
+ const payload = await res.json();
263
+ resultBox.textContent = JSON.stringify(payload, null, 2);
264
+ await refresh();
265
+ }}
266
+
267
+ async function runIntegrationCheck() {{
268
+ const resultBox = document.getElementById('integration-result');
269
+ const res = await fetch('/api/integrations/check');
270
+ const payload = await res.json();
271
+ resultBox.textContent = JSON.stringify(payload, null, 2);
272
+ }}
273
+
274
+ async function refresh() {{
275
+ const res = await fetch('/api/status');
276
+ const data = await res.json();
277
+ document.getElementById('phase').textContent = data.phase;
278
+ document.getElementById('coverage').textContent = JSON.stringify(data.coverage, null, 2);
279
+ const body = document.getElementById('tasks');
280
+ body.replaceChildren(...(data.tasks || []).map(t => {{
281
+ const row = document.createElement('tr');
282
+ row.appendChild(setText(document.createElement('td'), t.id));
283
+ row.appendChild(setText(document.createElement('td'), t.status));
284
+ row.appendChild(setText(document.createElement('td'), t.title));
285
+ return row;
286
+ }}));
287
+ const integrationsBody = document.getElementById('integrations');
288
+ const capabilities = ((data.integrations || {{}}).capabilities || []);
289
+ integrationsBody.replaceChildren(...capabilities.map(item => {{
290
+ const row = document.createElement('tr');
291
+ row.appendChild(setText(document.createElement('td'), item.label || item.name));
292
+ row.appendChild(setText(document.createElement('td'), item.on_path ? 'yes' : 'no'));
293
+ row.appendChild(setText(document.createElement('td'), item.mcp ? 'yes' : 'no'));
294
+ row.appendChild(setText(document.createElement('td'), item.hooks ? 'yes' : 'verification'));
295
+ row.appendChild(setText(document.createElement('td'), item.launcher_shim ? 'yes' : 'no'));
296
+ row.appendChild(statusCell(item.config_status));
297
+ row.appendChild(setText(document.createElement('td'), item.notes || ''));
298
+ const action = document.createElement('td');
299
+ const button = document.createElement('button');
300
+ button.type = 'button';
301
+ button.dataset.target = item.apply_target || item.name;
302
+ button.textContent = item.config_status === 'ok' ? 'Reapply' : 'Fix';
303
+ button.disabled = !item.fixable;
304
+ action.appendChild(button);
305
+ row.appendChild(action);
306
+ return row;
307
+ }}));
308
+
309
+ const runsBody = document.getElementById('runs');
310
+ runsBody.replaceChildren(...(data.recent_runs || []).map(run => {{
311
+ const row = document.createElement('tr');
312
+ row.appendChild(setText(document.createElement('td'), run.run_id));
313
+ row.appendChild(setText(document.createElement('td'), run.task_id));
314
+ row.appendChild(setText(document.createElement('td'), run.agent));
315
+ row.appendChild(setText(document.createElement('td'), run.status || 'unknown'));
316
+ row.appendChild(setText(document.createElement('td'), run.transcript || ''));
317
+ return row;
318
+ }}));
319
+
320
+ document.getElementById('events').textContent = JSON.stringify(data.events || [], null, 2);
321
+ }}
322
+
323
+ document.addEventListener('click', event => {{
324
+ const button = event.target;
325
+ if (!(button instanceof HTMLButtonElement)) return;
326
+ if (button.id === 'run-check') {{
327
+ runIntegrationCheck();
328
+ return;
329
+ }}
330
+ const target = button.dataset.target;
331
+ if (target) applyIntegration(target);
332
+ }});
333
+
334
+ refresh();
335
+ setInterval(refresh, 2000);
336
+ </script>
337
+ </body>
338
+ </html>"""
339
+
340
+
341
+ def run_dashboard(project_root: Path, host: str = "127.0.0.1", port: int = 8765) -> None:
342
+ dashboard_token = secrets.token_urlsafe(24)
343
+
344
+ class DashboardServer(ThreadingHTTPServer):
345
+ allow_reuse_address = True
346
+ daemon_threads = True
347
+
348
+ class Handler(BaseHTTPRequestHandler):
349
+ def do_GET(self): # noqa: N802
350
+ parsed = urlparse(self.path)
351
+ if parsed.path == f"/assets/{LOGO_ASSET}":
352
+ body = logo_asset_bytes()
353
+ self.send_response(200)
354
+ self.send_header("Content-Type", "image/png")
355
+ self.send_header("Cache-Control", "public, max-age=3600")
356
+ self.send_header("Content-Length", str(len(body)))
357
+ self.end_headers()
358
+ self.wfile.write(body)
359
+ return
360
+ if parsed.path == f"/assets/{LEGACY_LOGO_ASSET}":
361
+ body = logo_svg().encode("utf-8")
362
+ self.send_response(200)
363
+ self.send_header("Content-Type", "image/svg+xml; charset=utf-8")
364
+ self.send_header("Cache-Control", "public, max-age=3600")
365
+ self.send_header("Content-Length", str(len(body)))
366
+ self.end_headers()
367
+ self.wfile.write(body)
368
+ return
369
+ if parsed.path == "/api/integrations/check":
370
+ body = json.dumps(build_integration_check_report(project_root).as_dict()).encode("utf-8")
371
+ self.send_response(200)
372
+ self.send_header("Content-Type", "application/json")
373
+ self.send_header("Content-Length", str(len(body)))
374
+ self.end_headers()
375
+ self.wfile.write(body)
376
+ return
377
+ if parsed.path == "/api/status":
378
+ body = json.dumps(dashboard_payload(project_root)).encode("utf-8")
379
+ self.send_response(200)
380
+ self.send_header("Content-Type", "application/json")
381
+ self.send_header("Content-Length", str(len(body)))
382
+ self.end_headers()
383
+ self.wfile.write(body)
384
+ return
385
+ if parsed.path.startswith("/api/"):
386
+ body = json.dumps({"error": "Not found"}).encode("utf-8")
387
+ self.send_response(404)
388
+ self.send_header("Content-Type", "application/json")
389
+ self.send_header("Content-Length", str(len(body)))
390
+ self.end_headers()
391
+ self.wfile.write(body)
392
+ return
393
+ body = dashboard_html(dashboard_token).encode("utf-8")
394
+ self.send_response(200)
395
+ self.send_header("Content-Type", "text/html; charset=utf-8")
396
+ self.send_header("Content-Length", str(len(body)))
397
+ self.end_headers()
398
+ self.wfile.write(body)
399
+
400
+ def do_POST(self): # noqa: N802
401
+ parsed = urlparse(self.path)
402
+ if parsed.path != "/api/integrations/apply":
403
+ _json_response(self, 404, {"ok": False, "error": "Not found"})
404
+ return
405
+ if not _is_loopback_client(self):
406
+ _json_response(self, 403, {"ok": False, "error": "dashboard mutations require loopback client"})
407
+ return
408
+ length = int(self.headers.get("Content-Length") or "0")
409
+ raw_body = self.rfile.read(length)
410
+ provided = self.headers.get("X-DevCouncil-Dashboard-Token")
411
+ payload = dashboard_apply_payload(
412
+ project_root,
413
+ raw_body,
414
+ token=dashboard_token,
415
+ provided_token=provided,
416
+ )
417
+ _json_response(self, 200 if payload.get("ok") else 403, payload)
418
+
419
+ def log_message(self, format, *args): # noqa: A002
420
+ return
421
+
422
+ server = DashboardServer((host, port), Handler)
423
+ server.serve_forever()
@@ -1 +1 @@
1
- """Utilities package: redaction, paths, hashing, subprocess helpers."""
1
+ """Utilities package: redaction, paths, hashing, subprocess helpers."""