code-coordinator 0.5.46__py3-none-any.whl

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 (295) hide show
  1. code_coordinator-0.5.46.dist-info/METADATA +625 -0
  2. code_coordinator-0.5.46.dist-info/RECORD +295 -0
  3. code_coordinator-0.5.46.dist-info/WHEEL +5 -0
  4. code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
  5. code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
  6. code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
  7. coord/__init__.py +176 -0
  8. coord/_board_mapping.py +229 -0
  9. coord/acceptance.py +468 -0
  10. coord/acceptance_drivers.py +632 -0
  11. coord/agent.py +7517 -0
  12. coord/agent_app.py +1555 -0
  13. coord/agent_update.py +417 -0
  14. coord/agents/opencode/.gitignore +13 -0
  15. coord/agents/opencode/agents/work.md +129 -0
  16. coord/agents/opencode/routing.jsonc +49 -0
  17. coord/audit.py +301 -0
  18. coord/auto_loop.py +1440 -0
  19. coord/board_bool_guard.py +72 -0
  20. coord/board_service.py +141 -0
  21. coord/board_wire.py +309 -0
  22. coord/brain.py +581 -0
  23. coord/branch_model.py +214 -0
  24. coord/cargo_cache.py +258 -0
  25. coord/ci_github.py +386 -0
  26. coord/ci_store.py +560 -0
  27. coord/claim.py +353 -0
  28. coord/cli.py +454 -0
  29. coord/client.py +610 -0
  30. coord/commands/__init__.py +1 -0
  31. coord/commands/_common.py +329 -0
  32. coord/commands/acceptance.py +916 -0
  33. coord/commands/agent_ops.py +1339 -0
  34. coord/commands/audit.py +131 -0
  35. coord/commands/chat.py +320 -0
  36. coord/commands/dispatch.py +1780 -0
  37. coord/commands/dispatch_workers.py +4894 -0
  38. coord/commands/drive.py +616 -0
  39. coord/commands/drive_queue.py +1203 -0
  40. coord/commands/gate_a.py +217 -0
  41. coord/commands/gates.py +89 -0
  42. coord/commands/issues.py +681 -0
  43. coord/commands/lifecycle.py +513 -0
  44. coord/commands/merge.py +1900 -0
  45. coord/commands/milestone.py +2081 -0
  46. coord/commands/plan_followup.py +1243 -0
  47. coord/commands/plans.py +156 -0
  48. coord/commands/release.py +2232 -0
  49. coord/commands/report.py +341 -0
  50. coord/commands/review.py +1523 -0
  51. coord/commands/scorecard.py +252 -0
  52. coord/commands/sessions.py +1930 -0
  53. coord/commands/setup.py +576 -0
  54. coord/commands/status.py +2089 -0
  55. coord/commands/terminal.py +385 -0
  56. coord/commands/test_gate.py +775 -0
  57. coord/commands/tui.py +288 -0
  58. coord/comments.py +718 -0
  59. coord/config.py +3032 -0
  60. coord/conflict_fix.py +633 -0
  61. coord/dao.py +483 -0
  62. coord/dashboard/__init__.py +0 -0
  63. coord/dashboard/fixture.py +376 -0
  64. coord/dashboard/index.html +658 -0
  65. coord/dashboard/server.py +1894 -0
  66. coord/dashboard/terminal.py +382 -0
  67. coord/dashboard/webapp/.gitignore +9 -0
  68. coord/dashboard/webapp/components.json +17 -0
  69. coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
  70. coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
  71. coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
  72. coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
  73. coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
  74. coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
  75. coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
  76. coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
  77. coord/dashboard/webapp/dist/icons/icon.svg +5 -0
  78. coord/dashboard/webapp/dist/index.html +38 -0
  79. coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
  80. coord/dashboard/webapp/dist/sw.js +1 -0
  81. coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
  82. coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
  83. coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
  84. coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
  85. coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
  86. coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
  87. coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
  88. coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
  89. coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
  90. coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
  91. coord/dashboard/webapp/eslint.config.js +20 -0
  92. coord/dashboard/webapp/index.html +37 -0
  93. coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
  94. coord/dashboard/webapp/package-lock.json +10584 -0
  95. coord/dashboard/webapp/package.json +63 -0
  96. coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
  97. coord/dashboard/webapp/playwright.config.ts +93 -0
  98. coord/dashboard/webapp/postcss.config.js +6 -0
  99. coord/dashboard/webapp/public/icons/icon-192.png +0 -0
  100. coord/dashboard/webapp/public/icons/icon-512.png +0 -0
  101. coord/dashboard/webapp/public/icons/icon.svg +5 -0
  102. coord/dashboard/webapp/src/App.tsx +140 -0
  103. coord/dashboard/webapp/src/api/client.ts +199 -0
  104. coord/dashboard/webapp/src/api/generated.ts +176 -0
  105. coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
  106. coord/dashboard/webapp/src/components/Detail.tsx +800 -0
  107. coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
  108. coord/dashboard/webapp/src/components/Home.tsx +435 -0
  109. coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
  110. coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
  111. coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
  112. coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
  113. coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
  114. coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
  115. coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
  116. coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
  117. coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
  118. coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
  119. coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
  120. coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
  121. coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
  122. coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
  123. coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
  124. coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
  125. coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
  126. coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
  127. coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
  128. coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
  129. coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
  130. coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
  131. coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
  132. coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
  133. coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
  134. coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
  135. coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
  136. coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
  137. coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
  138. coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
  139. coord/dashboard/webapp/src/index.css +210 -0
  140. coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
  141. coord/dashboard/webapp/src/lib/utils.ts +6 -0
  142. coord/dashboard/webapp/src/main.tsx +46 -0
  143. coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
  144. coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
  145. coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
  146. coord/dashboard/webapp/src/realtime/connection.ts +227 -0
  147. coord/dashboard/webapp/src/realtime/events.ts +100 -0
  148. coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
  149. coord/dashboard/webapp/src/routes/paths.ts +92 -0
  150. coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
  151. coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
  152. coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
  153. coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
  154. coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
  155. coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
  156. coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
  157. coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
  158. coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
  159. coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
  160. coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
  161. coord/dashboard/webapp/src/shell/railItems.ts +105 -0
  162. coord/dashboard/webapp/src/shell/shellState.ts +174 -0
  163. coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
  164. coord/dashboard/webapp/src/test-setup.ts +41 -0
  165. coord/dashboard/webapp/src/vite-env.d.ts +2 -0
  166. coord/dashboard/webapp/tailwind.config.js +140 -0
  167. coord/dashboard/webapp/tsconfig.json +25 -0
  168. coord/dashboard/webapp/tsconfig.node.json +11 -0
  169. coord/dashboard/webapp/vite.config.ts +71 -0
  170. coord/db.py +1076 -0
  171. coord/dead_end.py +332 -0
  172. coord/deploy/README.md +33 -0
  173. coord/deploy/coord-agent.service +89 -0
  174. coord/deploy/coord-db-backup.service +60 -0
  175. coord/deploy/coord-db-backup.sh +74 -0
  176. coord/deploy/coord-db-backup.timer +18 -0
  177. coord/deploy/coord-drive-queue.service +117 -0
  178. coord/deploy/coord-drive-queue.timer +39 -0
  179. coord/deploy/coord-notify.service +48 -0
  180. coord/deploy/coord-notify.timer +24 -0
  181. coord/deploy/coord-release-propagate.service +83 -0
  182. coord/deploy/coord-release-propagate.timer +38 -0
  183. coord/deploy/coord-release-window.service +119 -0
  184. coord/deploy/coord-release-window.timer +36 -0
  185. coord/deploy/coord-serve.service +82 -0
  186. coord/deploy/coord-web-dist-build.service +43 -0
  187. coord/deploy/coord-web-dist-build.timer +36 -0
  188. coord/deploy/coord-web.service +125 -0
  189. coord/deploy_manifest.py +80 -0
  190. coord/deploy_units.py +384 -0
  191. coord/deps.py +115 -0
  192. coord/diagnose.py +1623 -0
  193. coord/dispatch.py +1009 -0
  194. coord/dist_name.py +123 -0
  195. coord/drive.py +3101 -0
  196. coord/drive_queue.py +2298 -0
  197. coord/drive_state.py +870 -0
  198. coord/events.py +381 -0
  199. coord/failure_class.py +914 -0
  200. coord/filelock.py +168 -0
  201. coord/fleet_config_health.py +300 -0
  202. coord/freshness.py +206 -0
  203. coord/gate_a.py +469 -0
  204. coord/gate_b.py +411 -0
  205. coord/gate_snapshot.py +385 -0
  206. coord/gates.py +582 -0
  207. coord/github_ops.py +1954 -0
  208. coord/goal.py +125 -0
  209. coord/graph_health.py +348 -0
  210. coord/health/__init__.py +69 -0
  211. coord/health/aggregate.py +129 -0
  212. coord/health/checks/__init__.py +13 -0
  213. coord/health/checks/agent_install.py +280 -0
  214. coord/health/checks/cargo_targets.py +171 -0
  215. coord/health/checks/claude_binary.py +65 -0
  216. coord/health/checks/deploy_lane_facts.py +458 -0
  217. coord/health/checks/disk.py +99 -0
  218. coord/health/checks/fleet_board.py +89 -0
  219. coord/health/checks/fleet_deploy_lanes.py +469 -0
  220. coord/health/checks/fleet_phantom.py +69 -0
  221. coord/health/checks/fleet_unit_drift.py +151 -0
  222. coord/health/checks/graph.py +192 -0
  223. coord/health/checks/plan_usage.py +88 -0
  224. coord/health/checks/repo_state.py +161 -0
  225. coord/health/checks/spawned_coord.py +465 -0
  226. coord/health/checks/timer_active.py +254 -0
  227. coord/health/checks/toolchain.py +547 -0
  228. coord/health/checks/unit_drift.py +648 -0
  229. coord/health/checks/unit_enablement.py +171 -0
  230. coord/health/checks/worktrees.py +96 -0
  231. coord/health/cli.py +121 -0
  232. coord/health/context.py +106 -0
  233. coord/health/fleet_snapshot.py +477 -0
  234. coord/health/models.py +250 -0
  235. coord/health/pypi.py +231 -0
  236. coord/health/registry.py +240 -0
  237. coord/health/render.py +82 -0
  238. coord/health/units.py +60 -0
  239. coord/hooks.py +106 -0
  240. coord/housekeeping.py +204 -0
  241. coord/interactive.py +4286 -0
  242. coord/issue_store.py +1496 -0
  243. coord/liveness_auditor.py +293 -0
  244. coord/machine_pause.py +755 -0
  245. coord/merge_queue.py +4681 -0
  246. coord/milestone_chat.py +600 -0
  247. coord/milestone_dispatch.py +943 -0
  248. coord/milestone_gate.py +709 -0
  249. coord/milestone_order.py +840 -0
  250. coord/mock_author.py +334 -0
  251. coord/models.py +891 -0
  252. coord/network.py +269 -0
  253. coord/new_issue_chat.py +229 -0
  254. coord/notify.py +3226 -0
  255. coord/openapi.py +404 -0
  256. coord/overlap_fence.py +133 -0
  257. coord/parentage.py +200 -0
  258. coord/parentage_github.py +58 -0
  259. coord/pipeline.py +481 -0
  260. coord/plan_parser.py +266 -0
  261. coord/plans.py +543 -0
  262. coord/platform_paths.py +43 -0
  263. coord/pr_body_lint.py +67 -0
  264. coord/prereqs.py +533 -0
  265. coord/progress.py +425 -0
  266. coord/providers/__init__.py +683 -0
  267. coord/providers/base.py +218 -0
  268. coord/providers/claude.py +284 -0
  269. coord/providers/claude_pty.py +610 -0
  270. coord/providers/opencode.py +896 -0
  271. coord/reconcile.py +2233 -0
  272. coord/refine_chat.py +485 -0
  273. coord/release_cordon.py +525 -0
  274. coord/release_propagate.py +1176 -0
  275. coord/release_verify.py +777 -0
  276. coord/release_window.py +322 -0
  277. coord/reports.py +1643 -0
  278. coord/revalidate.py +1101 -0
  279. coord/review.py +3317 -0
  280. coord/scorecard.py +484 -0
  281. coord/serve_app.py +7192 -0
  282. coord/skills/update-issue/SKILL.md +93 -0
  283. coord/smoke.py +1030 -0
  284. coord/split_work.py +210 -0
  285. coord/stage_projection.py +650 -0
  286. coord/state.py +5720 -0
  287. coord/test_author.py +1064 -0
  288. coord/test_chat.py +352 -0
  289. coord/test_orchestrator.py +494 -0
  290. coord/test_report.py +178 -0
  291. coord/tui_release.py +271 -0
  292. coord/usage.py +753 -0
  293. coord/usage_limits.py +358 -0
  294. coord/usage_rollup.py +709 -0
  295. coord/worker_events.py +954 -0
coord/parentage.py ADDED
@@ -0,0 +1,200 @@
1
+ """Parentage seam (#1195) — backend-agnostic parent/child API over the
2
+ issue-store seam.
3
+
4
+ Every gap in the "Epic as a first-class parent" milestone is downstream of
5
+ parentage being **plain text** today: the epic->child relation is the ``##
6
+ Work order`` / ``## Sub-issues`` markdown checklist
7
+ :mod:`coord.milestone_order` parses (:func:`coord.milestone_order.
8
+ parse_sub_issues`). GitHub's REST sub-issues API is live on this repo but
9
+ used nowhere — there is no structural parent link for anything to reconcile
10
+ against.
11
+
12
+ This module is the narrow contract the future :issue:`183` pluggable
13
+ ``IssueStore`` refactor will slot a real backend-selection policy into. Until
14
+ then it ships two concrete adapters behind the identical
15
+ :class:`ParentageStore` shape:
16
+
17
+ * :class:`coord.parentage_github.GitHubParentage` — the live GitHub
18
+ sub-issues REST API (``GET``/``POST``/``DELETE .../sub_issues``).
19
+ * :class:`MarkdownParentage` — the ``## Sub-issues`` checklist fallback for
20
+ GitLab / SQLite / no-API backends (and, today, the cheap path the
21
+ board-payload publish step uses — see ``coord/serve_app.py``'s ``board()``
22
+ handler — since a live API call per pipeline row per poll isn't
23
+ affordable).
24
+
25
+ Callers reach either backend through the same method names/signatures so
26
+ neither the board-payload builder nor a future ``coord`` command has to
27
+ branch on which one is live — that's precisely what keeps #183 (pluggable
28
+ issue store) viable.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from dataclasses import dataclass
34
+ from typing import Protocol, runtime_checkable
35
+
36
+ from coord.milestone_order import parse_sub_issues, parse_work_order
37
+
38
+
39
+ __all__ = [
40
+ "Child",
41
+ "ParentRef",
42
+ "ParentageStore",
43
+ "MarkdownParentage",
44
+ "build_parentage_store",
45
+ ]
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Child:
50
+ """One child of an issue: its number and open/closed state."""
51
+
52
+ number: int
53
+ state: str # "open" | "closed"
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class ParentRef:
58
+ """The parent of an issue: its number and open/closed state."""
59
+
60
+ number: int
61
+ state: str # "open" | "closed"
62
+
63
+
64
+ @runtime_checkable
65
+ class ParentageStore(Protocol):
66
+ """Read/write parent-child relations for one issue-tracker backend.
67
+
68
+ ``body``/``epics`` are backend-specific extra context a caller may
69
+ already have on hand (e.g. from the same cached ``issues`` row the
70
+ board-payload builder already fetched) — a backend that doesn't need
71
+ them (GitHub, which asks the live API) simply ignores them. Every
72
+ adapter accepts the same keyword names so a caller can pass everything
73
+ it might need without knowing which backend answers.
74
+ """
75
+
76
+ def children(
77
+ self, repo_github: str, issue_number: int, *, body: str = "",
78
+ ) -> list[Child]: ...
79
+
80
+ def parent(
81
+ self,
82
+ repo_github: str,
83
+ issue_number: int,
84
+ *,
85
+ epics: list[dict] | None = None,
86
+ ) -> ParentRef | None: ...
87
+
88
+ def add_child(self, repo_github: str, parent_number: int, child_number: int) -> None: ...
89
+
90
+ def remove_child(self, repo_github: str, parent_number: int, child_number: int) -> None: ...
91
+
92
+
93
+ class MarkdownParentage:
94
+ """Fallback adapter (#1195): the ``## Sub-issues`` checklist convention
95
+ #1008 already established for backends with no native sub-issues API
96
+ (GitLab / SQLite / no-API).
97
+
98
+ ``children()`` parses *body* — the tracking issue's OWN body — with
99
+ :func:`coord.milestone_order.parse_sub_issues`. No network call, so this
100
+ is also what the board-payload publish step uses even on a GitHub-backed
101
+ repo: a live sub-issues API call per epic per poll isn't affordable, and
102
+ (until EP-2 backfills existing epics) the markdown checklist is the only
103
+ populated source of truth anyway.
104
+
105
+ #1197 fix-iteration finding: an "epic" tracking issue's ``## Work
106
+ order`` block (#767/#645 — the milestone's DAG of work items, written by
107
+ ``coord milestone write-order``) already names the exact same
108
+ parent->children relationship the newer ``## Sub-issues`` checklist
109
+ (#1008, written only by the separate, opt-in ``coord milestone
110
+ add-child``) is meant to encode — but plenty of existing epics (this
111
+ milestone's own #1200 tracking issue included) predate #1008 and were
112
+ never additionally seeded with a ``## Sub-issues`` block, so
113
+ ``parse_sub_issues`` legitimately finds nothing even though the epic
114
+ unambiguously has children. Pass ``fallback_to_work_order=True`` to
115
+ treat an empty/absent ``## Sub-issues`` block as "fall back to ``##
116
+ Work order``" instead of "no children" — this is what the board-payload
117
+ children projection (the Pipeline nesting feature) opts into; it
118
+ defaults to ``False`` so the #1196 close-guard (:func:`coord.
119
+ github_ops.get_open_children`) keeps its narrower, deliberately
120
+ conservative behavior unchanged.
121
+
122
+ ``parent()`` has no reverse pointer to walk — the checklist lives on the
123
+ PARENT's body, not the child's — so callers must supply *epics*: every
124
+ candidate tracking issue as ``{"number": int, "state": str, "body":
125
+ str}``. The first whose ``## Sub-issues`` checklist names *issue_number*
126
+ is the parent; ``None`` when no candidate does (or none were supplied).
127
+
128
+ The checklist's ``- [x]`` / ``- [ ]`` box is the only completion signal
129
+ this convention carries, so it doubles as the child's reported ``state``
130
+ here (``"closed"`` for checked, ``"open"`` otherwise) — an approximation
131
+ of the real GitHub issue state, good enough for nesting/display, not a
132
+ live lookup.
133
+ """
134
+
135
+ def children(
136
+ self,
137
+ repo_github: str,
138
+ issue_number: int,
139
+ *,
140
+ body: str = "",
141
+ fallback_to_work_order: bool = False,
142
+ ) -> list[Child]:
143
+ work_order = parse_sub_issues(body)
144
+ if not work_order.nodes and fallback_to_work_order:
145
+ work_order = parse_work_order(body)
146
+ return [
147
+ Child(number=n.issue_number, state="closed" if n.checked else "open")
148
+ for n in work_order.nodes
149
+ if n.issue_number != issue_number # defensive: never self-nest
150
+ ]
151
+
152
+ def parent(
153
+ self,
154
+ repo_github: str,
155
+ issue_number: int,
156
+ *,
157
+ epics: list[dict] | None = None,
158
+ ) -> ParentRef | None:
159
+ for epic in epics or []:
160
+ try:
161
+ work_order = parse_sub_issues(epic.get("body") or "")
162
+ except Exception: # noqa: BLE001 — a malformed epic body: skip it
163
+ continue
164
+ if work_order.node(issue_number) is not None:
165
+ number = epic.get("number")
166
+ if number is None:
167
+ continue
168
+ return ParentRef(number=int(number), state=str(epic.get("state") or "open"))
169
+ return None
170
+
171
+ def add_child(self, repo_github: str, parent_number: int, child_number: int) -> None:
172
+ raise NotImplementedError(
173
+ "MarkdownParentage is read-only from this seam — the `## "
174
+ "Sub-issues` checklist is mutated via `coord milestone "
175
+ "add-child` (#1008), the existing write path for this "
176
+ "convention (splices the section idempotently, unlike a plain "
177
+ "body overwrite)."
178
+ )
179
+
180
+ def remove_child(self, repo_github: str, parent_number: int, child_number: int) -> None:
181
+ raise NotImplementedError(
182
+ "MarkdownParentage is read-only from this seam — remove a "
183
+ "sub-issue via `coord milestone add-child ... --remove` (#1008)."
184
+ )
185
+
186
+
187
+ def build_parentage_store(backend: str) -> ParentageStore:
188
+ """Construct the :class:`ParentageStore` named by *backend*.
189
+
190
+ Centralised here (mirrors :func:`coord.ci_store.build_ci_store`) so
191
+ callers don't branch on the config value themselves. Unknown values fall
192
+ back to :class:`MarkdownParentage` — the always-available, no-network
193
+ adapter — so a typo in config can't crash a caller the way a KeyError
194
+ would.
195
+ """
196
+ if backend == "github":
197
+ from coord.parentage_github import GitHubParentage # noqa: PLC0415
198
+
199
+ return GitHubParentage()
200
+ return MarkdownParentage()
@@ -0,0 +1,58 @@
1
+ """GitHub sub-issues backend for :mod:`coord.parentage` (#1195).
2
+
3
+ Shells out to ``gh api`` via :mod:`coord.github_ops`'s sub-issues wrappers.
4
+ Mirrors the :class:`coord.ci_store.CiStore` / :class:`coord.ci_github.GitHubCi`
5
+ split: the Protocol + pure fallback adapter live in ``coord.parentage``, and
6
+ this module is the live-API adapter, kept separate so a test can import the
7
+ Protocol/dataclasses without dragging in ``github_ops``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from coord import github_ops
13
+ from coord.parentage import Child, ParentRef
14
+
15
+
16
+ class GitHubParentage:
17
+ """Live adapter over the GitHub sub-issues REST API.
18
+
19
+ ``body``/``epics`` are accepted for :class:`coord.parentage.
20
+ ParentageStore` shape parity with :class:`coord.parentage.
21
+ MarkdownParentage` but ignored here — every read is a live ``gh api``
22
+ call, so there's nothing cached to fall back to.
23
+ """
24
+
25
+ def children(
26
+ self, repo_github: str, issue_number: int, *, body: str = "",
27
+ ) -> list[Child]:
28
+ raw = github_ops.get_sub_issues(repo_github, issue_number)
29
+ children: list[Child] = []
30
+ for entry in raw:
31
+ if not isinstance(entry, dict):
32
+ continue
33
+ number = entry.get("number")
34
+ if number is None:
35
+ continue
36
+ children.append(Child(number=int(number), state=str(entry.get("state") or "open")))
37
+ return children
38
+
39
+ def parent(
40
+ self,
41
+ repo_github: str,
42
+ issue_number: int,
43
+ *,
44
+ epics: list[dict] | None = None,
45
+ ) -> ParentRef | None:
46
+ raw = github_ops.get_issue_parent(repo_github, issue_number)
47
+ if not raw:
48
+ return None
49
+ number = raw.get("number")
50
+ if number is None:
51
+ return None
52
+ return ParentRef(number=int(number), state=str(raw.get("state") or "open"))
53
+
54
+ def add_child(self, repo_github: str, parent_number: int, child_number: int) -> None:
55
+ github_ops.add_sub_issue(repo_github, parent_number, child_number)
56
+
57
+ def remove_child(self, repo_github: str, parent_number: int, child_number: int) -> None:
58
+ github_ops.remove_sub_issue(repo_github, parent_number, child_number)