flonat-research 0.1.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 (285) hide show
  1. package/.claude/agents/domain-reviewer.md +336 -0
  2. package/.claude/agents/fixer.md +226 -0
  3. package/.claude/agents/paper-critic.md +370 -0
  4. package/.claude/agents/peer-reviewer.md +289 -0
  5. package/.claude/agents/proposal-reviewer.md +215 -0
  6. package/.claude/agents/referee2-reviewer.md +367 -0
  7. package/.claude/agents/references/journal-referee-profiles.md +354 -0
  8. package/.claude/agents/references/paper-critic/council-personas.md +77 -0
  9. package/.claude/agents/references/paper-critic/council-prompts.md +198 -0
  10. package/.claude/agents/references/peer-reviewer/report-template.md +199 -0
  11. package/.claude/agents/references/peer-reviewer/sa-prompts.md +260 -0
  12. package/.claude/agents/references/peer-reviewer/security-scan.md +188 -0
  13. package/.claude/agents/references/proposal-reviewer/report-template.md +144 -0
  14. package/.claude/agents/references/proposal-reviewer/sa-prompts.md +149 -0
  15. package/.claude/agents/references/referee-config.md +114 -0
  16. package/.claude/agents/references/referee2-reviewer/audit-checklists.md +287 -0
  17. package/.claude/agents/references/referee2-reviewer/report-template.md +334 -0
  18. package/.claude/rules/design-before-results.md +52 -0
  19. package/.claude/rules/ignore-agents-md.md +17 -0
  20. package/.claude/rules/ignore-gemini-md.md +17 -0
  21. package/.claude/rules/lean-claude-md.md +45 -0
  22. package/.claude/rules/learn-tags.md +99 -0
  23. package/.claude/rules/overleaf-separation.md +67 -0
  24. package/.claude/rules/plan-first.md +175 -0
  25. package/.claude/rules/read-docs-first.md +50 -0
  26. package/.claude/rules/scope-discipline.md +28 -0
  27. package/.claude/settings.json +125 -0
  28. package/.context/current-focus.md +33 -0
  29. package/.context/preferences/priorities.md +36 -0
  30. package/.context/preferences/task-naming.md +28 -0
  31. package/.context/profile.md +29 -0
  32. package/.context/projects/_index.md +41 -0
  33. package/.context/projects/papers/nudge-exp.md +22 -0
  34. package/.context/projects/papers/uncertainty.md +31 -0
  35. package/.context/resources/claude-scientific-writer-review.md +48 -0
  36. package/.context/resources/cunningham-multi-analyst-agents.md +104 -0
  37. package/.context/resources/cunningham-multilang-code-audit.md +62 -0
  38. package/.context/resources/google-ai-co-scientist-review.md +72 -0
  39. package/.context/resources/karpathy-llm-council-review.md +58 -0
  40. package/.context/resources/multi-coder-reliability-protocol.md +175 -0
  41. package/.context/resources/pedro-santanna-takeaways.md +96 -0
  42. package/.context/resources/venue-rankings/abs_ajg_2024.csv +1823 -0
  43. package/.context/resources/venue-rankings/abs_ajg_2024_econ.csv +356 -0
  44. package/.context/resources/venue-rankings/cabs_4_4star_theory.csv +40 -0
  45. package/.context/resources/venue-rankings/core_2026.csv +801 -0
  46. package/.context/resources/venue-rankings.md +147 -0
  47. package/.context/workflows/README.md +69 -0
  48. package/.context/workflows/daily-review.md +91 -0
  49. package/.context/workflows/meeting-actions.md +108 -0
  50. package/.context/workflows/replication-protocol.md +155 -0
  51. package/.context/workflows/weekly-review.md +113 -0
  52. package/.mcp-server-biblio/formatters.py +158 -0
  53. package/.mcp-server-biblio/pyproject.toml +11 -0
  54. package/.mcp-server-biblio/server.py +678 -0
  55. package/.mcp-server-biblio/sources/__init__.py +14 -0
  56. package/.mcp-server-biblio/sources/base.py +73 -0
  57. package/.mcp-server-biblio/sources/formatters.py +83 -0
  58. package/.mcp-server-biblio/sources/models.py +22 -0
  59. package/.mcp-server-biblio/sources/multi_source.py +243 -0
  60. package/.mcp-server-biblio/sources/openalex_source.py +183 -0
  61. package/.mcp-server-biblio/sources/scopus_source.py +309 -0
  62. package/.mcp-server-biblio/sources/wos_source.py +508 -0
  63. package/.mcp-server-biblio/uv.lock +896 -0
  64. package/.scripts/README.md +161 -0
  65. package/.scripts/ai_pattern_density.py +446 -0
  66. package/.scripts/conf +445 -0
  67. package/.scripts/config.py +122 -0
  68. package/.scripts/count_inventory.py +275 -0
  69. package/.scripts/daily_digest.py +288 -0
  70. package/.scripts/done +177 -0
  71. package/.scripts/extract_meeting_actions.py +223 -0
  72. package/.scripts/focus +176 -0
  73. package/.scripts/generate-codex-agents-md.py +217 -0
  74. package/.scripts/inbox +194 -0
  75. package/.scripts/notion_helpers.py +325 -0
  76. package/.scripts/openalex/query_helpers.py +306 -0
  77. package/.scripts/papers +227 -0
  78. package/.scripts/query +223 -0
  79. package/.scripts/session-history.py +201 -0
  80. package/.scripts/skill-health.py +516 -0
  81. package/.scripts/skill-log-miner.py +273 -0
  82. package/.scripts/sync-to-codex.sh +252 -0
  83. package/.scripts/task +213 -0
  84. package/.scripts/tasks +190 -0
  85. package/.scripts/week +206 -0
  86. package/CLAUDE.md +197 -0
  87. package/LICENSE +21 -0
  88. package/MEMORY.md +38 -0
  89. package/README.md +269 -0
  90. package/docs/agents.md +44 -0
  91. package/docs/bibliography-setup.md +55 -0
  92. package/docs/council-mode.md +36 -0
  93. package/docs/getting-started.md +245 -0
  94. package/docs/hooks.md +38 -0
  95. package/docs/mcp-servers.md +82 -0
  96. package/docs/notion-setup.md +109 -0
  97. package/docs/rules.md +33 -0
  98. package/docs/scripts.md +303 -0
  99. package/docs/setup-overview/setup-overview.pdf +0 -0
  100. package/docs/skills.md +70 -0
  101. package/docs/system.md +159 -0
  102. package/hooks/block-destructive-git.sh +66 -0
  103. package/hooks/context-monitor.py +114 -0
  104. package/hooks/postcompact-restore.py +157 -0
  105. package/hooks/precompact-autosave.py +181 -0
  106. package/hooks/promise-checker.sh +124 -0
  107. package/hooks/protect-source-files.sh +81 -0
  108. package/hooks/resume-context-loader.sh +53 -0
  109. package/hooks/startup-context-loader.sh +102 -0
  110. package/package.json +51 -0
  111. package/packages/cli-council/.github/workflows/claude-code-review.yml +44 -0
  112. package/packages/cli-council/.github/workflows/claude.yml +50 -0
  113. package/packages/cli-council/README.md +100 -0
  114. package/packages/cli-council/pyproject.toml +43 -0
  115. package/packages/cli-council/src/cli_council/__init__.py +19 -0
  116. package/packages/cli-council/src/cli_council/__main__.py +185 -0
  117. package/packages/cli-council/src/cli_council/backends/__init__.py +8 -0
  118. package/packages/cli-council/src/cli_council/backends/base.py +81 -0
  119. package/packages/cli-council/src/cli_council/backends/claude.py +25 -0
  120. package/packages/cli-council/src/cli_council/backends/codex.py +27 -0
  121. package/packages/cli-council/src/cli_council/backends/gemini.py +26 -0
  122. package/packages/cli-council/src/cli_council/checkpoint.py +212 -0
  123. package/packages/cli-council/src/cli_council/config.py +51 -0
  124. package/packages/cli-council/src/cli_council/council.py +391 -0
  125. package/packages/cli-council/src/cli_council/models.py +46 -0
  126. package/packages/llm-council/.github/workflows/claude-code-review.yml +44 -0
  127. package/packages/llm-council/.github/workflows/claude.yml +50 -0
  128. package/packages/llm-council/README.md +453 -0
  129. package/packages/llm-council/pyproject.toml +42 -0
  130. package/packages/llm-council/src/llm_council/__init__.py +23 -0
  131. package/packages/llm-council/src/llm_council/__main__.py +259 -0
  132. package/packages/llm-council/src/llm_council/checkpoint.py +193 -0
  133. package/packages/llm-council/src/llm_council/client.py +253 -0
  134. package/packages/llm-council/src/llm_council/config.py +232 -0
  135. package/packages/llm-council/src/llm_council/council.py +482 -0
  136. package/packages/llm-council/src/llm_council/models.py +46 -0
  137. package/packages/mcp-bibliography/MEMORY.md +31 -0
  138. package/packages/mcp-bibliography/_app.py +226 -0
  139. package/packages/mcp-bibliography/formatters.py +158 -0
  140. package/packages/mcp-bibliography/log/2026-03-13-2100.md +35 -0
  141. package/packages/mcp-bibliography/pyproject.toml +15 -0
  142. package/packages/mcp-bibliography/run.sh +20 -0
  143. package/packages/mcp-bibliography/scholarly_formatters.py +83 -0
  144. package/packages/mcp-bibliography/server.py +1857 -0
  145. package/packages/mcp-bibliography/tools/__init__.py +28 -0
  146. package/packages/mcp-bibliography/tools/_registry.py +19 -0
  147. package/packages/mcp-bibliography/tools/altmetric.py +107 -0
  148. package/packages/mcp-bibliography/tools/core.py +92 -0
  149. package/packages/mcp-bibliography/tools/dblp.py +52 -0
  150. package/packages/mcp-bibliography/tools/openalex.py +296 -0
  151. package/packages/mcp-bibliography/tools/opencitations.py +102 -0
  152. package/packages/mcp-bibliography/tools/openreview.py +179 -0
  153. package/packages/mcp-bibliography/tools/orcid.py +131 -0
  154. package/packages/mcp-bibliography/tools/scholarly.py +575 -0
  155. package/packages/mcp-bibliography/tools/unpaywall.py +63 -0
  156. package/packages/mcp-bibliography/tools/zenodo.py +123 -0
  157. package/packages/mcp-bibliography/uv.lock +711 -0
  158. package/scripts/setup.sh +143 -0
  159. package/skills/beamer-deck/SKILL.md +199 -0
  160. package/skills/beamer-deck/references/quality-rubric.md +54 -0
  161. package/skills/beamer-deck/references/review-prompts.md +106 -0
  162. package/skills/bib-validate/SKILL.md +261 -0
  163. package/skills/bib-validate/references/council-mode.md +34 -0
  164. package/skills/bib-validate/references/deep-verify.md +79 -0
  165. package/skills/bib-validate/references/fix-mode.md +36 -0
  166. package/skills/bib-validate/references/openalex-verification.md +45 -0
  167. package/skills/bib-validate/references/preprint-check.md +31 -0
  168. package/skills/bib-validate/references/ref-manager-crossref.md +41 -0
  169. package/skills/bib-validate/references/report-template.md +82 -0
  170. package/skills/code-archaeology/SKILL.md +141 -0
  171. package/skills/code-review/SKILL.md +265 -0
  172. package/skills/code-review/references/quality-rubric.md +67 -0
  173. package/skills/consolidate-memory/SKILL.md +208 -0
  174. package/skills/context-status/SKILL.md +126 -0
  175. package/skills/creation-guard/SKILL.md +230 -0
  176. package/skills/devils-advocate/SKILL.md +130 -0
  177. package/skills/devils-advocate/references/competing-hypotheses.md +83 -0
  178. package/skills/init-project/SKILL.md +115 -0
  179. package/skills/init-project-course/references/memory-and-settings.md +92 -0
  180. package/skills/init-project-course/references/organise-templates.md +94 -0
  181. package/skills/init-project-course/skill.md +147 -0
  182. package/skills/init-project-light/skill.md +139 -0
  183. package/skills/init-project-research/SKILL.md +368 -0
  184. package/skills/init-project-research/references/atlas-pipeline-sync.md +70 -0
  185. package/skills/init-project-research/references/atlas-schema.md +81 -0
  186. package/skills/init-project-research/references/confirmation-report.md +39 -0
  187. package/skills/init-project-research/references/domain-profile-template.md +104 -0
  188. package/skills/init-project-research/references/interview-round3.md +34 -0
  189. package/skills/init-project-research/references/literature-discovery.md +43 -0
  190. package/skills/init-project-research/references/scaffold-details.md +197 -0
  191. package/skills/init-project-research/templates/field-calibration.md +60 -0
  192. package/skills/init-project-research/templates/pipeline-manifest.md +63 -0
  193. package/skills/init-project-research/templates/run-all.sh +116 -0
  194. package/skills/init-project-research/templates/seed-files.md +337 -0
  195. package/skills/insights-deck/SKILL.md +151 -0
  196. package/skills/interview-me/SKILL.md +157 -0
  197. package/skills/latex/SKILL.md +141 -0
  198. package/skills/latex/references/latex-configs.md +183 -0
  199. package/skills/latex-autofix/SKILL.md +230 -0
  200. package/skills/latex-autofix/references/known-errors.md +183 -0
  201. package/skills/latex-autofix/references/quality-rubric.md +50 -0
  202. package/skills/latex-health-check/SKILL.md +161 -0
  203. package/skills/learn/SKILL.md +220 -0
  204. package/skills/learn/scripts/validate_skill.py +265 -0
  205. package/skills/lessons-learned/SKILL.md +201 -0
  206. package/skills/literature/SKILL.md +335 -0
  207. package/skills/literature/references/agent-templates.md +393 -0
  208. package/skills/literature/references/bibliometric-apis.md +44 -0
  209. package/skills/literature/references/cli-council-search.md +79 -0
  210. package/skills/literature/references/openalex-api-guide.md +371 -0
  211. package/skills/literature/references/openalex-common-queries.md +381 -0
  212. package/skills/literature/references/openalex-workflows.md +248 -0
  213. package/skills/literature/references/reference-manager-sync.md +36 -0
  214. package/skills/literature/references/scopus-api-guide.md +208 -0
  215. package/skills/literature/references/wos-api-guide.md +308 -0
  216. package/skills/multi-perspective/SKILL.md +311 -0
  217. package/skills/multi-perspective/references/computational-many-analysts.md +77 -0
  218. package/skills/pipeline-manifest/SKILL.md +226 -0
  219. package/skills/pre-submission-report/SKILL.md +153 -0
  220. package/skills/process-reviews/SKILL.md +244 -0
  221. package/skills/process-reviews/references/rr-routing.md +101 -0
  222. package/skills/project-deck/SKILL.md +87 -0
  223. package/skills/project-safety/SKILL.md +135 -0
  224. package/skills/proofread/SKILL.md +254 -0
  225. package/skills/proofread/references/quality-rubric.md +104 -0
  226. package/skills/python-env/SKILL.md +57 -0
  227. package/skills/quarto-deck/SKILL.md +226 -0
  228. package/skills/quarto-deck/references/markdown-format.md +143 -0
  229. package/skills/quarto-deck/references/quality-rubric.md +54 -0
  230. package/skills/save-context/SKILL.md +174 -0
  231. package/skills/session-log/SKILL.md +98 -0
  232. package/skills/shared/concept-validation-gate.md +161 -0
  233. package/skills/shared/council-protocol.md +265 -0
  234. package/skills/shared/distribution-diagnostics.md +164 -0
  235. package/skills/shared/engagement-stratified-sampling.md +218 -0
  236. package/skills/shared/escalation-protocol.md +74 -0
  237. package/skills/shared/external-audit-protocol.md +205 -0
  238. package/skills/shared/intercoder-reliability.md +256 -0
  239. package/skills/shared/mcp-degradation.md +81 -0
  240. package/skills/shared/method-probing-questions.md +163 -0
  241. package/skills/shared/multi-language-conventions.md +143 -0
  242. package/skills/shared/paid-api-safety.md +174 -0
  243. package/skills/shared/palettes.md +90 -0
  244. package/skills/shared/progressive-disclosure.md +92 -0
  245. package/skills/shared/project-documentation-content.md +443 -0
  246. package/skills/shared/project-documentation-format.md +281 -0
  247. package/skills/shared/project-documentation.md +100 -0
  248. package/skills/shared/publication-output.md +138 -0
  249. package/skills/shared/quality-scoring.md +70 -0
  250. package/skills/shared/reference-resolution.md +77 -0
  251. package/skills/shared/research-quality-rubric.md +165 -0
  252. package/skills/shared/rhetoric-principles.md +54 -0
  253. package/skills/shared/skill-design-patterns.md +272 -0
  254. package/skills/shared/skill-index.md +240 -0
  255. package/skills/shared/system-documentation.md +334 -0
  256. package/skills/shared/tikz-rules.md +402 -0
  257. package/skills/shared/validation-tiers.md +121 -0
  258. package/skills/shared/venue-guides/README.md +46 -0
  259. package/skills/shared/venue-guides/cell_press_style.md +483 -0
  260. package/skills/shared/venue-guides/conferences_formatting.md +564 -0
  261. package/skills/shared/venue-guides/cs_conference_style.md +463 -0
  262. package/skills/shared/venue-guides/examples/cell_summary_example.md +247 -0
  263. package/skills/shared/venue-guides/examples/medical_structured_abstract.md +313 -0
  264. package/skills/shared/venue-guides/examples/nature_abstract_examples.md +213 -0
  265. package/skills/shared/venue-guides/examples/neurips_introduction_example.md +245 -0
  266. package/skills/shared/venue-guides/journals_formatting.md +486 -0
  267. package/skills/shared/venue-guides/medical_journal_styles.md +535 -0
  268. package/skills/shared/venue-guides/ml_conference_style.md +556 -0
  269. package/skills/shared/venue-guides/nature_science_style.md +405 -0
  270. package/skills/shared/venue-guides/reviewer_expectations.md +417 -0
  271. package/skills/shared/venue-guides/venue_writing_styles.md +321 -0
  272. package/skills/split-pdf/SKILL.md +172 -0
  273. package/skills/split-pdf/methodology.md +48 -0
  274. package/skills/sync-notion/SKILL.md +93 -0
  275. package/skills/system-audit/SKILL.md +157 -0
  276. package/skills/system-audit/references/sub-agent-prompts.md +294 -0
  277. package/skills/task-management/SKILL.md +131 -0
  278. package/skills/update-focus/SKILL.md +204 -0
  279. package/skills/update-project-doc/SKILL.md +194 -0
  280. package/skills/validate-bib/SKILL.md +242 -0
  281. package/skills/validate-bib/references/council-mode.md +34 -0
  282. package/skills/validate-bib/references/deep-verify.md +71 -0
  283. package/skills/validate-bib/references/openalex-verification.md +45 -0
  284. package/skills/validate-bib/references/preprint-check.md +31 -0
  285. package/skills/validate-bib/references/report-template.md +62 -0
@@ -0,0 +1,81 @@
1
+ """Abstract base for CLI backends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import os
8
+ import shutil
9
+ from abc import ABC, abstractmethod
10
+ from time import perf_counter
11
+
12
+ from cli_council.config import STAGE_TIMEOUT
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class CLIBackend(ABC):
18
+ """Base class for CLI tool backends."""
19
+
20
+ name: str = ""
21
+ command: str = ""
22
+
23
+ @abstractmethod
24
+ def build_args(self, prompt: str, *, model: str | None = None) -> list[str]:
25
+ """Build the full argument list for subprocess execution."""
26
+
27
+ def is_available(self) -> bool:
28
+ """Check if the CLI tool is installed and on PATH."""
29
+ return shutil.which(self.command) is not None
30
+
31
+ async def run(
32
+ self,
33
+ prompt: str,
34
+ *,
35
+ model: str | None = None,
36
+ timeout: int = STAGE_TIMEOUT,
37
+ cwd: str | None = None,
38
+ ) -> tuple[str, int]:
39
+ """Execute the CLI tool and return (output_text, elapsed_ms).
40
+
41
+ Raises RuntimeError if the command fails or times out.
42
+ """
43
+ args = self.build_args(prompt, model=model)
44
+ logger.info("Running %s: %s", self.name, " ".join(args[:4]) + "...")
45
+
46
+ # Clean environment: unset CLAUDECODE to allow nested claude calls
47
+ env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
48
+
49
+ t0 = perf_counter()
50
+ try:
51
+ proc = await asyncio.create_subprocess_exec(
52
+ *args,
53
+ stdout=asyncio.subprocess.PIPE,
54
+ stderr=asyncio.subprocess.PIPE,
55
+ cwd=cwd,
56
+ env=env,
57
+ )
58
+ stdout, stderr = await asyncio.wait_for(
59
+ proc.communicate(), timeout=timeout,
60
+ )
61
+ except asyncio.TimeoutError:
62
+ proc.kill()
63
+ elapsed = int((perf_counter() - t0) * 1000)
64
+ raise RuntimeError(
65
+ f"{self.name} timed out after {timeout}s"
66
+ ) from None
67
+
68
+ elapsed = int((perf_counter() - t0) * 1000)
69
+ output = stdout.decode("utf-8", errors="replace").strip()
70
+
71
+ if proc.returncode != 0:
72
+ err = stderr.decode("utf-8", errors="replace").strip()
73
+ logger.warning(
74
+ "%s exited with code %d: %s",
75
+ self.name, proc.returncode, err[:200],
76
+ )
77
+ # Some backends write useful output to stderr on non-zero exit
78
+ if not output and err:
79
+ output = err
80
+
81
+ return output, elapsed
@@ -0,0 +1,25 @@
1
+ """Claude Code CLI backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cli_council.backends.base import CLIBackend
6
+ from cli_council.config import BACKENDS
7
+
8
+
9
+ class ClaudeBackend(CLIBackend):
10
+ """Wrapper for Anthropic's Claude Code CLI (claude -p)."""
11
+
12
+ name = "claude"
13
+ command = "claude"
14
+
15
+ def __init__(self, model: str | None = None) -> None:
16
+ spec = BACKENDS["claude"]
17
+ self.default_model = model or spec.default_model
18
+
19
+ def build_args(self, prompt: str, *, model: str | None = None) -> list[str]:
20
+ effective_model = model or self.default_model
21
+ return [
22
+ "claude",
23
+ "-p", prompt,
24
+ "--model", effective_model,
25
+ ]
@@ -0,0 +1,27 @@
1
+ """Codex CLI backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cli_council.backends.base import CLIBackend
6
+ from cli_council.config import BACKENDS
7
+
8
+
9
+ class CodexBackend(CLIBackend):
10
+ """Wrapper for OpenAI's Codex CLI (codex exec)."""
11
+
12
+ name = "codex"
13
+ command = "codex"
14
+
15
+ def __init__(self, model: str | None = None) -> None:
16
+ spec = BACKENDS["codex"]
17
+ self.default_model = model or spec.default_model
18
+
19
+ def build_args(self, prompt: str, *, model: str | None = None) -> list[str]:
20
+ effective_model = model or self.default_model
21
+ args = ["codex", "exec"]
22
+ # Skip -m flag when using default — ChatGPT accounts don't support
23
+ # explicit model selection but use the best available (GPT-5).
24
+ if effective_model != "default":
25
+ args.extend(["-m", effective_model])
26
+ args.extend(["--full-auto", prompt])
27
+ return args
@@ -0,0 +1,26 @@
1
+ """Gemini CLI backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cli_council.backends.base import CLIBackend
6
+ from cli_council.config import BACKENDS
7
+
8
+
9
+ class GeminiBackend(CLIBackend):
10
+ """Wrapper for Google's Gemini CLI (gemini -p)."""
11
+
12
+ name = "gemini"
13
+ command = "gemini"
14
+
15
+ def __init__(self, model: str | None = None) -> None:
16
+ spec = BACKENDS["gemini"]
17
+ self.default_model = model or spec.default_model
18
+
19
+ def build_args(self, prompt: str, *, model: str | None = None) -> list[str]:
20
+ effective_model = model or self.default_model
21
+ return [
22
+ "gemini",
23
+ "-p", prompt,
24
+ "-m", effective_model,
25
+ "-o", "text",
26
+ ]
@@ -0,0 +1,212 @@
1
+ """Checkpoint and file-based coordination for council runs.
2
+
3
+ Inspired by:
4
+ - Owlex (agentic-mcp-tools/owlex): session resumption across deliberation rounds
5
+ - agents-council (MrLesk/agents-council): atomic file writes, cursor-based state
6
+
7
+ Provides:
8
+ - Atomic JSON writes (tmp + fsync + rename) for crash safety
9
+ - Stage checkpointing: save after each stage, resume from last completed stage
10
+ - Pending participant tracking
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import logging
17
+ import os
18
+ import tempfile
19
+ from dataclasses import dataclass, field
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Default checkpoint directory (gitignored in projects)
26
+ DEFAULT_CHECKPOINT_DIR = ".council"
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class CheckpointMeta:
31
+ """Metadata for a checkpoint file."""
32
+
33
+ run_id: str
34
+ stage: int # 1, 2, or 3
35
+ timestamp: str
36
+ backends: list[str]
37
+
38
+
39
+ def _atomic_write_json(path: Path, data: dict) -> None:
40
+ """Write JSON atomically: tmp file -> fsync -> rename.
41
+
42
+ If the process crashes mid-write, the original file is preserved.
43
+ """
44
+ path.parent.mkdir(parents=True, exist_ok=True)
45
+ fd, tmp_path = tempfile.mkstemp(
46
+ dir=str(path.parent), suffix=".tmp", prefix=".ckpt-",
47
+ )
48
+ try:
49
+ with os.fdopen(fd, "w") as f:
50
+ json.dump(data, f, indent=2, default=str)
51
+ f.flush()
52
+ os.fsync(f.fileno())
53
+ os.rename(tmp_path, str(path))
54
+ except BaseException:
55
+ # Clean up temp file on any failure
56
+ try:
57
+ os.unlink(tmp_path)
58
+ except OSError:
59
+ pass
60
+ raise
61
+
62
+
63
+ def _read_json(path: Path) -> dict | None:
64
+ """Read a JSON checkpoint file, returning None if missing or corrupt."""
65
+ if not path.exists():
66
+ return None
67
+ try:
68
+ return json.loads(path.read_text())
69
+ except (json.JSONDecodeError, OSError) as exc:
70
+ logger.warning("Corrupt checkpoint %s: %s", path, exc)
71
+ return None
72
+
73
+
74
+ def _make_run_id() -> str:
75
+ """Generate a timestamped run ID."""
76
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
77
+
78
+
79
+ @dataclass
80
+ class CouncilCheckpointer:
81
+ """Manages checkpoint state for a council run.
82
+
83
+ Usage:
84
+ ckpt = CouncilCheckpointer(checkpoint_dir="/path/to/.council")
85
+ # Saves after each stage; can resume from last completed stage.
86
+ """
87
+
88
+ checkpoint_dir: Path
89
+ run_id: str = field(default_factory=_make_run_id)
90
+
91
+ def __post_init__(self) -> None:
92
+ self.checkpoint_dir = Path(self.checkpoint_dir)
93
+
94
+ # ---- Save ----
95
+
96
+ def save_stage1(self, assessments: list[dict], backends: list[str]) -> Path:
97
+ """Save Stage 1 assessments to checkpoint."""
98
+ path = self._stage_path(1)
99
+ data = {
100
+ "meta": {
101
+ "run_id": self.run_id,
102
+ "stage": 1,
103
+ "timestamp": datetime.now(timezone.utc).isoformat(),
104
+ "backends": backends,
105
+ },
106
+ "assessments": assessments,
107
+ }
108
+ _atomic_write_json(path, data)
109
+ logger.info("Checkpoint saved: Stage 1 → %s", path)
110
+ return path
111
+
112
+ def save_stage2(
113
+ self,
114
+ peer_reviews: list[dict],
115
+ backends: list[str],
116
+ aggregate_rankings: list[dict] | None = None,
117
+ ) -> Path:
118
+ """Save Stage 2 peer reviews to checkpoint."""
119
+ path = self._stage_path(2)
120
+ data = {
121
+ "meta": {
122
+ "run_id": self.run_id,
123
+ "stage": 2,
124
+ "timestamp": datetime.now(timezone.utc).isoformat(),
125
+ "backends": backends,
126
+ },
127
+ "peer_reviews": peer_reviews,
128
+ "aggregate_rankings": aggregate_rankings or [],
129
+ }
130
+ _atomic_write_json(path, data)
131
+ logger.info("Checkpoint saved: Stage 2 → %s", path)
132
+ return path
133
+
134
+ def save_stage3(self, synthesis: dict | str, chairman: str) -> Path:
135
+ """Save Stage 3 synthesis to checkpoint."""
136
+ path = self._stage_path(3)
137
+ data = {
138
+ "meta": {
139
+ "run_id": self.run_id,
140
+ "stage": 3,
141
+ "timestamp": datetime.now(timezone.utc).isoformat(),
142
+ "chairman": chairman,
143
+ },
144
+ "synthesis": synthesis,
145
+ }
146
+ _atomic_write_json(path, data)
147
+ logger.info("Checkpoint saved: Stage 3 → %s", path)
148
+ return path
149
+
150
+ # ---- Load / Resume ----
151
+
152
+ def last_completed_stage(self) -> int:
153
+ """Return the highest completed stage (0 if none)."""
154
+ for stage in (3, 2, 1):
155
+ if self._stage_path(stage).exists():
156
+ return stage
157
+ return 0
158
+
159
+ def load_stage1(self) -> list[dict] | None:
160
+ """Load Stage 1 assessments from checkpoint."""
161
+ data = _read_json(self._stage_path(1))
162
+ if data is None:
163
+ return None
164
+ return data.get("assessments")
165
+
166
+ def load_stage2(self) -> tuple[list[dict], list[dict]] | None:
167
+ """Load Stage 2 peer reviews + aggregate rankings from checkpoint."""
168
+ data = _read_json(self._stage_path(2))
169
+ if data is None:
170
+ return None
171
+ return data.get("peer_reviews", []), data.get("aggregate_rankings", [])
172
+
173
+ def load_stage3(self) -> dict | str | None:
174
+ """Load Stage 3 synthesis from checkpoint."""
175
+ data = _read_json(self._stage_path(3))
176
+ if data is None:
177
+ return None
178
+ return data.get("synthesis")
179
+
180
+ # ---- Pending participants ----
181
+
182
+ def pending_participants(
183
+ self, all_backends: list[str], responded: list[str],
184
+ ) -> list[str]:
185
+ """Return backends that haven't responded yet."""
186
+ return [b for b in all_backends if b not in set(responded)]
187
+
188
+ # ---- Discovery ----
189
+
190
+ def find_latest_run(self) -> str | None:
191
+ """Find the most recent run_id in the checkpoint directory."""
192
+ if not self.checkpoint_dir.exists():
193
+ return None
194
+ # Look for stage-1 files (every run has one)
195
+ stage1_files = sorted(self.checkpoint_dir.glob("*-stage1.json"), reverse=True)
196
+ if not stage1_files:
197
+ return None
198
+ # Extract run_id from filename: {run_id}-stage1.json
199
+ return stage1_files[0].stem.replace("-stage1", "")
200
+
201
+ def clean(self) -> None:
202
+ """Remove all checkpoint files for this run."""
203
+ for stage in (1, 2, 3):
204
+ path = self._stage_path(stage)
205
+ if path.exists():
206
+ path.unlink()
207
+ logger.info("Removed checkpoint: %s", path)
208
+
209
+ # ---- Internal ----
210
+
211
+ def _stage_path(self, stage: int) -> Path:
212
+ return self.checkpoint_dir / f"{self.run_id}-stage{stage}.json"
@@ -0,0 +1,51 @@
1
+ """Backend registry and defaults."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class BackendSpec:
10
+ """Specification for a CLI backend."""
11
+
12
+ name: str
13
+ command: str # binary name
14
+ headless_flag: str # flag for non-interactive mode
15
+ model_flag: str # flag to select model (empty if not supported)
16
+ default_model: str # default model name
17
+ output_flag: str # flag for output format (empty if not needed)
18
+
19
+
20
+ BACKENDS: dict[str, BackendSpec] = {
21
+ "gemini": BackendSpec(
22
+ name="gemini",
23
+ command="gemini",
24
+ headless_flag="-p",
25
+ model_flag="-m",
26
+ default_model="gemini-2.5-pro",
27
+ output_flag="-o text",
28
+ ),
29
+ "codex": BackendSpec(
30
+ name="codex",
31
+ command="codex",
32
+ headless_flag="exec",
33
+ model_flag="-m",
34
+ default_model="default",
35
+ output_flag="",
36
+ ),
37
+ "claude": BackendSpec(
38
+ name="claude",
39
+ command="claude",
40
+ headless_flag="-p",
41
+ model_flag="--model",
42
+ default_model="claude-sonnet-4-6",
43
+ output_flag="",
44
+ ),
45
+ }
46
+
47
+ DEFAULT_COUNCIL_BACKENDS: list[str] = ["gemini", "codex", "claude"]
48
+ DEFAULT_CHAIRMAN: str = "claude"
49
+
50
+ # Subprocess timeout in seconds per stage
51
+ STAGE_TIMEOUT: int = 120