devcouncil 0.1.1 → 0.3.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 (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,184 @@
1
+ """Host hardware detection used to size local (Ollama) models.
2
+
3
+ DevCouncil runs the council roles against whatever LLM provider is configured.
4
+ For the local ``ollama`` provider the model has to fit in host memory. On Apple
5
+ Silicon Macs that memory is *unified* (shared by CPU/GPU/OS), so total RAM is the
6
+ ceiling. On a host with a discrete GPU (NVIDIA), Ollama offloads to VRAM, so the
7
+ *VRAM* is the practical ceiling instead — a 64 GB box with an 8 GB GPU should not be
8
+ told to run a model that only fits in system RAM. This module exposes small, pure
9
+ helpers so ``dev doctor`` and ``dev setup`` can recommend a model that will actually
10
+ run instead of a one-size-fits-all default, on macOS, Linux and Windows.
11
+
12
+ Everything here is best-effort and stdlib-only: detection failures return
13
+ ``None`` rather than raising, so callers degrade to the static default.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import os
19
+ import platform
20
+ import shutil
21
+ import subprocess
22
+ from dataclasses import dataclass
23
+
24
+ # Recommended Ollama context window for DevCouncil's large planning prompts.
25
+ # Kept in sync with the value surfaced by `dev doctor`.
26
+ RECOMMENDED_NUM_CTX = 16384
27
+
28
+ # Fallback model when the host is unknown or has little memory. Matches the
29
+ # static defaults in ``llm/model_defaults.yaml``.
30
+ DEFAULT_OLLAMA_MODEL = "qwen2.5-coder:7b"
31
+
32
+ # RAM (GiB) -> recommended qwen2.5-coder size. Highest matching tier wins.
33
+ # Sizes are chosen so the quantized weights plus a 16k context comfortably fit
34
+ # alongside the OS in Apple Silicon unified memory.
35
+ _OLLAMA_MODEL_TIERS: tuple[tuple[float, str], ...] = (
36
+ (48.0, "qwen2.5-coder:32b"),
37
+ (24.0, "qwen2.5-coder:14b"),
38
+ (0.0, "qwen2.5-coder:7b"),
39
+ )
40
+
41
+
42
+ def is_macos() -> bool:
43
+ return platform.system() == "Darwin"
44
+
45
+
46
+ def is_apple_silicon() -> bool:
47
+ """True on Apple-Silicon (arm64) Macs."""
48
+ return is_macos() and platform.machine() == "arm64"
49
+
50
+
51
+ def mac_chip_brand() -> str | None:
52
+ """Marketing CPU string on macOS (e.g. ``Apple M3 Pro``), else None."""
53
+ if not is_macos():
54
+ return None
55
+ try:
56
+ out = subprocess.check_output(
57
+ ["sysctl", "-n", "machdep.cpu.brand_string"],
58
+ text=True,
59
+ timeout=5,
60
+ ).strip()
61
+ return out or None
62
+ except Exception:
63
+ return None
64
+
65
+
66
+ def total_ram_gb() -> float | None:
67
+ """Total physical RAM in GiB, or None if it cannot be determined."""
68
+ try:
69
+ if is_macos():
70
+ out = subprocess.check_output(
71
+ ["sysctl", "-n", "hw.memsize"], text=True, timeout=5
72
+ ).strip()
73
+ return int(out) / (1024**3)
74
+ # POSIX (Linux): pages * page size.
75
+ pages = os.sysconf("SC_PHYS_PAGES")
76
+ page_size = os.sysconf("SC_PAGE_SIZE")
77
+ return (pages * page_size) / (1024**3)
78
+ except (OSError, ValueError, AttributeError, subprocess.SubprocessError):
79
+ return None
80
+
81
+
82
+ def nvidia_vram_gb() -> float | None:
83
+ """Total VRAM (GiB) of the largest NVIDIA GPU via ``nvidia-smi``, else ``None``.
84
+
85
+ On hosts with a discrete NVIDIA GPU this is the real ceiling for Ollama, since the
86
+ model is offloaded to VRAM. Returns ``None`` when ``nvidia-smi`` is absent (no GPU,
87
+ Apple Silicon, or an unsupported vendor), so callers fall back to total RAM."""
88
+ if not shutil.which("nvidia-smi"):
89
+ return None
90
+ try:
91
+ out = subprocess.check_output(
92
+ ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"],
93
+ text=True,
94
+ timeout=5,
95
+ stderr=subprocess.DEVNULL,
96
+ ).strip()
97
+ except (OSError, subprocess.SubprocessError):
98
+ return None
99
+ sizes_mib: list[float] = []
100
+ for line in out.splitlines():
101
+ line = line.strip()
102
+ if not line:
103
+ continue
104
+ try:
105
+ sizes_mib.append(float(line))
106
+ except ValueError:
107
+ continue
108
+ if not sizes_mib:
109
+ return None
110
+ return max(sizes_mib) / 1024.0 # MiB -> GiB
111
+
112
+
113
+ def recommend_ollama_model(ram_gb: float | None = None, vram_gb: float | None = None) -> str:
114
+ """Largest qwen2.5-coder size expected to run on the host.
115
+
116
+ When a discrete GPU's ``vram_gb`` is known it is the ceiling (Ollama offloads to
117
+ VRAM); otherwise total system RAM is used (correct for unified-memory Macs and
118
+ CPU-only hosts)."""
119
+ if ram_gb is None:
120
+ ram_gb = total_ram_gb()
121
+ effective = vram_gb if vram_gb is not None else ram_gb
122
+ if effective is None:
123
+ return DEFAULT_OLLAMA_MODEL
124
+ for floor, model in _OLLAMA_MODEL_TIERS:
125
+ if effective >= floor:
126
+ return model
127
+ return DEFAULT_OLLAMA_MODEL
128
+
129
+
130
+ @dataclass(frozen=True)
131
+ class HostSummary:
132
+ """A snapshot of the host relevant to local-model sizing."""
133
+
134
+ is_macos: bool
135
+ is_apple_silicon: bool
136
+ chip: str | None
137
+ ram_gb: float | None
138
+ recommended_ollama_model: str
139
+ vram_gb: float | None = None
140
+
141
+ @property
142
+ def ram_label(self) -> str:
143
+ return f"{self.ram_gb:.0f} GB" if self.ram_gb is not None else "unknown RAM"
144
+
145
+ @property
146
+ def vram_label(self) -> str | None:
147
+ return f"{self.vram_gb:.0f} GB VRAM" if self.vram_gb is not None else None
148
+
149
+ @property
150
+ def chip_label(self) -> str:
151
+ if self.chip:
152
+ return self.chip
153
+ if self.is_apple_silicon:
154
+ return "Apple Silicon"
155
+ if self.vram_gb is not None:
156
+ return "discrete GPU host"
157
+ return "this host"
158
+
159
+ @property
160
+ def platform_label(self) -> str:
161
+ """Human label for the sizing row across OSes."""
162
+ if self.is_macos:
163
+ return "Apple Silicon" if self.is_apple_silicon else "Mac (Intel)"
164
+ system = platform.system()
165
+ return system or "Host"
166
+
167
+ @property
168
+ def memory_label(self) -> str:
169
+ """Memory ceiling used for the recommendation (VRAM if discrete GPU, else RAM)."""
170
+ vram = self.vram_label
171
+ return f"{vram} (GPU)" if vram else self.ram_label
172
+
173
+
174
+ def describe_host() -> HostSummary:
175
+ ram = total_ram_gb()
176
+ vram = nvidia_vram_gb()
177
+ return HostSummary(
178
+ is_macos=is_macos(),
179
+ is_apple_silicon=is_apple_silicon(),
180
+ chip=mac_chip_brand(),
181
+ ram_gb=ram,
182
+ vram_gb=vram,
183
+ recommended_ollama_model=recommend_ollama_model(ram, vram),
184
+ )
@@ -59,7 +59,7 @@ class AstMatcher:
59
59
 
60
60
  def _has_tree_sitter(self) -> bool:
61
61
  try:
62
- import tree_sitter # noqa: F401
62
+ import tree_sitter # type: ignore[import-not-found] # noqa: F401
63
63
  except Exception:
64
64
  return False
65
65
  return True
@@ -71,12 +71,13 @@ class AstMatcher:
71
71
  language: str | None = None,
72
72
  kind: str | None = None,
73
73
  limit: int = 100,
74
+ files: list[Path] | None = None,
74
75
  ) -> list[AstMatch]:
75
76
  language = language.lower() if language else None
76
77
  kind = kind.lower() if kind else None
77
78
  limit = max(1, limit)
78
79
  matches: list[AstMatch] = []
79
- for path in self._candidate_files(language):
80
+ for path in self._candidate_files(language, files):
80
81
  try:
81
82
  text = path.read_text(encoding="utf-8")
82
83
  except (OSError, UnicodeDecodeError):
@@ -88,22 +89,31 @@ class AstMatcher:
88
89
  return matches[:limit]
89
90
  return matches[:limit]
90
91
 
91
- def _candidate_files(self, language: str | None) -> list[Path]:
92
+ def _candidate_files(self, language: str | None, files: list[Path] | None = None) -> list[Path]:
92
93
  allowed_exts = {
93
94
  ext for ext, ext_language in self._EXT_LANGUAGE.items()
94
95
  if language is None or ext_language == language
95
96
  }
96
- files: list[Path] = []
97
+ # When the caller already walked the tree (e.g. SemanticIndex.create_snapshot
98
+ # shares one traversal across all collectors), filter that list in memory
99
+ # instead of re-globbing. The filter is identical to the rglob path below.
100
+ if files is not None:
101
+ return sorted(
102
+ path for path in files
103
+ if path.is_file() and path.suffix.lower() in allowed_exts
104
+ and not any(part in self._IGNORED_DIRS for part in path.parts)
105
+ )
106
+ candidates: list[Path] = []
97
107
  try:
98
108
  for path in self.project_root.rglob("*"):
99
109
  if not path.is_file() or path.suffix.lower() not in allowed_exts:
100
110
  continue
101
111
  if any(part in self._IGNORED_DIRS for part in path.parts):
102
112
  continue
103
- files.append(path)
113
+ candidates.append(path)
104
114
  except OSError:
105
- return files
106
- return sorted(files)
115
+ return candidates
116
+ return sorted(candidates)
107
117
 
108
118
  def _match_file(self, rel: str, language: str, text: str, *, query: str, kind: str | None) -> list[AstMatch]:
109
119
  if language == "python":
@@ -16,7 +16,16 @@ class LspServerCandidate:
16
16
 
17
17
 
18
18
  class LspInspector:
19
- """Starter LSP integration focused on discovery and safe initialize payloads."""
19
+ """Language-server *detection* only DevCouncil does not run an LSP client.
20
+
21
+ This inspector exists to answer one honest question: which language servers
22
+ for the repo's languages are installed on PATH? It does NOT spawn a server,
23
+ does NOT speak the LSP wire protocol, and does NOT send the ``initialize``
24
+ handshake. ``starter_initialize_payload`` builds the JSON-RPC ``initialize``
25
+ request a *future* client would send, but it is never transmitted — it is
26
+ surfaced purely as a reference/starter payload, clearly labelled as such, so
27
+ no consumer mistakes detection for a live LSP capability.
28
+ """
20
29
 
21
30
  _LANGUAGE_SERVERS: dict[str, list[list[str]]] = {
22
31
  "python": [["pyright-langserver", "--stdio"], ["pylsp"]],
@@ -76,7 +85,14 @@ class LspInspector:
76
85
  )
77
86
  return candidates
78
87
 
79
- def initialize_request(self, language: str) -> dict[str, Any]:
88
+ def starter_initialize_payload(self, language: str) -> dict[str, Any]:
89
+ """Build the JSON-RPC ``initialize`` request a future LSP client *would* send.
90
+
91
+ This payload is NEVER sent — DevCouncil has no LSP client. It is provided
92
+ only as a reference/starter for anyone wiring up real LSP support. See the
93
+ ``initialize_requests`` block in :meth:`summary`, which carries the same
94
+ non-sent payloads under an explicit ``"_note"`` disclaimer.
95
+ """
80
96
  return {
81
97
  "jsonrpc": "2.0",
82
98
  "id": 1,
@@ -97,10 +113,32 @@ class LspInspector:
97
113
  },
98
114
  }
99
115
 
116
+ # Made explicit in every summary so no consumer reads this as a live LSP feature.
117
+ _DETECTION_ONLY_NOTE = (
118
+ "Detection only: DevCouncil checks which language servers are installed on "
119
+ "PATH; it does not run an LSP client or send any requests."
120
+ )
121
+ _STARTER_PAYLOAD_NOTE = (
122
+ "Starter payloads only — these initialize requests are NEVER sent. They are "
123
+ "a reference for wiring up a real LSP client later."
124
+ )
125
+
100
126
  def summary(self, files: list[str] | None = None) -> dict[str, Any]:
101
127
  candidates = self.server_candidates(files)
102
128
  return {
129
+ "mode": "detection-only",
130
+ "note": self._DETECTION_ONLY_NOTE,
103
131
  "languages": self.detect_languages(files),
132
+ "detected_servers": [
133
+ {
134
+ "language": candidate.language,
135
+ "command": candidate.command,
136
+ "available": candidate.available,
137
+ "reason": candidate.reason,
138
+ }
139
+ for candidate in candidates
140
+ ],
141
+ # Back-compat alias for "detected_servers" (older consumers/tests).
104
142
  "servers": [
105
143
  {
106
144
  "language": candidate.language,
@@ -111,8 +149,11 @@ class LspInspector:
111
149
  for candidate in candidates
112
150
  ],
113
151
  "initialize_requests": {
114
- language: self.initialize_request(language)
115
- for language in sorted({candidate.language for candidate in candidates})
152
+ "_note": self._STARTER_PAYLOAD_NOTE,
153
+ **{
154
+ language: self.starter_initialize_payload(language)
155
+ for language in sorted({candidate.language for candidate in candidates})
156
+ },
116
157
  },
117
158
  }
118
159