devcouncil 0.2.0 → 0.2.1

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 (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,28 +0,0 @@
1
- from functools import lru_cache
2
- from importlib import resources
3
- from typing import Dict
4
-
5
- import yaml
6
-
7
- MODEL_PRICING_RESOURCE = "model_pricing.yaml"
8
-
9
-
10
- @lru_cache(maxsize=1)
11
- def load_model_pricing() -> Dict[str, Dict[str, float]]:
12
- data = resources.files(__package__).joinpath(MODEL_PRICING_RESOURCE).read_text(encoding="utf-8")
13
- loaded = yaml.safe_load(data) or {}
14
- return {
15
- str(model): {
16
- "prompt_per_1k": float(pricing.get("prompt_per_1k", 0.0)),
17
- "completion_per_1k": float(pricing.get("completion_per_1k", 0.0)),
18
- }
19
- for model, pricing in loaded.items()
20
- if isinstance(pricing, dict)
21
- }
22
-
23
-
24
- def pricing_for_model(model: str, default: Dict[str, float] | None = None) -> Dict[str, float]:
25
- pricing = load_model_pricing().get(model)
26
- if pricing is not None:
27
- return pricing
28
- return default or {"prompt_per_1k": 0.0, "completion_per_1k": 0.0}
@@ -1,146 +0,0 @@
1
- import json
2
- import logging
3
- import uuid
4
- from datetime import datetime, timezone
5
- from pathlib import Path
6
- from typing import Any, Dict, Iterable, List, Optional, Tuple
7
-
8
- from pydantic import BaseModel, ConfigDict, Field
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- TRACE_SCHEMA_VERSION = "devcouncil.trace.v1"
13
-
14
-
15
- class TraceEvent(BaseModel):
16
- """Stable JSONL event shape for DevCouncil and external visualizers."""
17
-
18
- model_config = ConfigDict(populate_by_name=True, serialize_by_alias=True)
19
-
20
- schema_version: str = Field(TRACE_SCHEMA_VERSION, alias="schema")
21
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
22
- timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
23
- runtime: str = "devcouncil"
24
- type: str
25
- run_id: Optional[str] = None
26
- task_id: Optional[str] = None
27
- summary: str = ""
28
- details: Dict[str, Any] = Field(default_factory=dict)
29
-
30
- @classmethod
31
- def from_legacy(cls, raw: Dict[str, Any]) -> "TraceEvent":
32
- if raw.get("schema") == TRACE_SCHEMA_VERSION:
33
- return cls.model_validate(raw)
34
- raw_details = raw.get("details")
35
- details: Dict[str, Any] = raw_details if isinstance(raw_details, dict) else {}
36
- raw_task_id = details.get("task_id")
37
- raw_summary = details.get("summary")
38
- raw_run_id = raw.get("run_id")
39
- return cls(
40
- schema=TRACE_SCHEMA_VERSION,
41
- type=str(raw.get("type", "legacy_event")),
42
- run_id=raw_run_id if isinstance(raw_run_id, str) else None,
43
- task_id=raw_task_id if isinstance(raw_task_id, str) else None,
44
- summary=raw_summary if isinstance(raw_summary, str) else "",
45
- details=details,
46
- )
47
-
48
-
49
- class TraceLogger:
50
- """Manages appending execution traces to a local log file."""
51
-
52
- def __init__(self, project_root: Path):
53
- self.log_dir = project_root / ".devcouncil" / "logs"
54
- self.trace_file = self.log_dir / "traces.jsonl"
55
- self.log_dir.mkdir(parents=True, exist_ok=True)
56
-
57
- def log_event(
58
- self,
59
- event_type: str,
60
- details: Dict[str, Any],
61
- run_id: Optional[str] = None,
62
- task_id: Optional[str] = None,
63
- summary: str = "",
64
- ) -> TraceEvent:
65
- """Append an orchestration event trace."""
66
- trace = TraceEvent(
67
- schema=TRACE_SCHEMA_VERSION,
68
- type=event_type,
69
- details=details,
70
- run_id=run_id,
71
- task_id=task_id,
72
- summary=summary,
73
- )
74
- try:
75
- with open(self.trace_file, "a", encoding="utf-8") as f:
76
- f.write(trace.model_dump_json() + "\n")
77
- except Exception as e:
78
- logger.debug("Failed to write trace event %s: %s", event_type, e)
79
- return trace
80
-
81
-
82
- def read_trace_events(project_root: Path) -> Iterable[TraceEvent]:
83
- trace_file = project_root / ".devcouncil" / "logs" / "traces.jsonl"
84
- if not trace_file.exists():
85
- return []
86
-
87
- events: list[TraceEvent] = []
88
- for line in trace_file.read_text(encoding="utf-8").splitlines():
89
- if not line.strip():
90
- continue
91
- try:
92
- events.append(TraceEvent.from_legacy(json.loads(line)))
93
- except Exception as exc:
94
- logger.debug("Skipping invalid trace line: %s", exc)
95
- return events
96
-
97
-
98
- def read_trace_events_since(
99
- project_root: Path, cursor: Optional[int] = None
100
- ) -> Tuple[List[TraceEvent], int]:
101
- """Incrementally read trace events appended after ``cursor``.
102
-
103
- The cursor is a byte offset into the trace file, which is robust to appends
104
- (the file is append-only) and makes repeated polling O(new bytes) rather than
105
- O(all events). Returns ``(events, next_cursor)`` where ``next_cursor`` should
106
- be passed back on the next call to fetch only newer events.
107
-
108
- Never raises: on any error it returns an empty batch and a safe cursor. A
109
- ``cursor`` past the current end-of-file (e.g. the log was rotated/truncated)
110
- is treated as a reset so the caller still makes progress.
111
- """
112
- trace_file = project_root / ".devcouncil" / "logs" / "traces.jsonl"
113
- start = cursor if isinstance(cursor, int) and cursor >= 0 else 0
114
- if not trace_file.exists():
115
- return [], start
116
-
117
- try:
118
- size = trace_file.stat().st_size
119
- # File shrank (rotation/truncation) — restart from the beginning.
120
- if start > size:
121
- start = 0
122
-
123
- with open(trace_file, "rb") as f:
124
- f.seek(start)
125
- chunk = f.read()
126
- except Exception as exc:
127
- logger.debug("Failed incremental trace read: %s", exc)
128
- return [], start
129
-
130
- # Only consume up to the last complete line so a half-written final line is
131
- # re-read (not skipped) on the next poll once it is fully flushed.
132
- last_newline = chunk.rfind(b"\n")
133
- if last_newline == -1:
134
- return [], start
135
- consumed = chunk[: last_newline + 1]
136
- next_cursor = start + len(consumed)
137
-
138
- events: List[TraceEvent] = []
139
- for raw_line in consumed.decode("utf-8", errors="replace").splitlines():
140
- if not raw_line.strip():
141
- continue
142
- try:
143
- events.append(TraceEvent.from_legacy(json.loads(raw_line)))
144
- except Exception as exc:
145
- logger.debug("Skipping invalid trace line: %s", exc)
146
- return events, next_cursor
@@ -1,52 +0,0 @@
1
- import json
2
- from pathlib import Path
3
- from typing import Dict, Any
4
-
5
- from devcouncil.telemetry.pricing import pricing_for_model
6
-
7
- class TelemetryTracker:
8
- def __init__(self, project_root: Path):
9
- self.log_file = project_root / ".devcouncil" / "logs" / "telemetry.json"
10
- self.stats = self._load()
11
-
12
- def _load(self) -> Dict[str, Any]:
13
- if self.log_file.exists():
14
- try:
15
- with open(self.log_file, "r") as f:
16
- return json.load(f)
17
- except Exception:
18
- pass
19
- return {"total_cost": 0.0, "total_prompt_tokens": 0, "total_completion_tokens": 0, "models": {}}
20
-
21
- def _save(self):
22
- self.log_file.parent.mkdir(parents=True, exist_ok=True)
23
- with open(self.log_file, "w") as f:
24
- json.dump(self.stats, f, indent=2)
25
-
26
- def log_usage(self, model: str, usage: Dict[str, int], *, local: bool = False):
27
- prompt_tokens = usage.get("prompt_tokens", 0)
28
- completion_tokens = usage.get("completion_tokens", 0)
29
-
30
- if local:
31
- # On-device providers (Ollama) incur no per-token cost. Zero by provider, not
32
- # by model-id matching — consistent with telemetry/cost.py — so a local tag
33
- # that happens to collide with a priced entry is never billed.
34
- cost = 0.0
35
- else:
36
- rates = pricing_for_model(model)
37
- cost = (prompt_tokens / 1000.0) * rates["prompt_per_1k"] + (
38
- completion_tokens / 1000.0
39
- ) * rates["completion_per_1k"]
40
-
41
- self.stats["total_cost"] += cost
42
- self.stats["total_prompt_tokens"] += prompt_tokens
43
- self.stats["total_completion_tokens"] += completion_tokens
44
-
45
- if model not in self.stats["models"]:
46
- self.stats["models"][model] = {"cost": 0.0, "prompt_tokens": 0, "completion_tokens": 0}
47
-
48
- self.stats["models"][model]["cost"] += cost
49
- self.stats["models"][model]["prompt_tokens"] += prompt_tokens
50
- self.stats["models"][model]["completion_tokens"] += completion_tokens
51
-
52
- self._save()
@@ -1 +0,0 @@
1
- """Live DevCouncil dashboard helpers."""
@@ -1,423 +0,0 @@
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 +0,0 @@
1
- """Utilities package: redaction, paths, hashing, subprocess helpers."""