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.
- code_coordinator-0.5.46.dist-info/METADATA +625 -0
- code_coordinator-0.5.46.dist-info/RECORD +295 -0
- code_coordinator-0.5.46.dist-info/WHEEL +5 -0
- code_coordinator-0.5.46.dist-info/entry_points.txt +2 -0
- code_coordinator-0.5.46.dist-info/licenses/LICENSE +110 -0
- code_coordinator-0.5.46.dist-info/top_level.txt +1 -0
- coord/__init__.py +176 -0
- coord/_board_mapping.py +229 -0
- coord/acceptance.py +468 -0
- coord/acceptance_drivers.py +632 -0
- coord/agent.py +7517 -0
- coord/agent_app.py +1555 -0
- coord/agent_update.py +417 -0
- coord/agents/opencode/.gitignore +13 -0
- coord/agents/opencode/agents/work.md +129 -0
- coord/agents/opencode/routing.jsonc +49 -0
- coord/audit.py +301 -0
- coord/auto_loop.py +1440 -0
- coord/board_bool_guard.py +72 -0
- coord/board_service.py +141 -0
- coord/board_wire.py +309 -0
- coord/brain.py +581 -0
- coord/branch_model.py +214 -0
- coord/cargo_cache.py +258 -0
- coord/ci_github.py +386 -0
- coord/ci_store.py +560 -0
- coord/claim.py +353 -0
- coord/cli.py +454 -0
- coord/client.py +610 -0
- coord/commands/__init__.py +1 -0
- coord/commands/_common.py +329 -0
- coord/commands/acceptance.py +916 -0
- coord/commands/agent_ops.py +1339 -0
- coord/commands/audit.py +131 -0
- coord/commands/chat.py +320 -0
- coord/commands/dispatch.py +1780 -0
- coord/commands/dispatch_workers.py +4894 -0
- coord/commands/drive.py +616 -0
- coord/commands/drive_queue.py +1203 -0
- coord/commands/gate_a.py +217 -0
- coord/commands/gates.py +89 -0
- coord/commands/issues.py +681 -0
- coord/commands/lifecycle.py +513 -0
- coord/commands/merge.py +1900 -0
- coord/commands/milestone.py +2081 -0
- coord/commands/plan_followup.py +1243 -0
- coord/commands/plans.py +156 -0
- coord/commands/release.py +2232 -0
- coord/commands/report.py +341 -0
- coord/commands/review.py +1523 -0
- coord/commands/scorecard.py +252 -0
- coord/commands/sessions.py +1930 -0
- coord/commands/setup.py +576 -0
- coord/commands/status.py +2089 -0
- coord/commands/terminal.py +385 -0
- coord/commands/test_gate.py +775 -0
- coord/commands/tui.py +288 -0
- coord/comments.py +718 -0
- coord/config.py +3032 -0
- coord/conflict_fix.py +633 -0
- coord/dao.py +483 -0
- coord/dashboard/__init__.py +0 -0
- coord/dashboard/fixture.py +376 -0
- coord/dashboard/index.html +658 -0
- coord/dashboard/server.py +1894 -0
- coord/dashboard/terminal.py +382 -0
- coord/dashboard/webapp/.gitignore +9 -0
- coord/dashboard/webapp/components.json +17 -0
- coord/dashboard/webapp/dist/assets/Gallery-da3qNiIw.js +71 -0
- coord/dashboard/webapp/dist/assets/Terminal-9CEnUXvW.css +32 -0
- coord/dashboard/webapp/dist/assets/Terminal-skVFCxPU.js +63 -0
- coord/dashboard/webapp/dist/assets/index-DltfZR5f.js +184 -0
- coord/dashboard/webapp/dist/assets/index-Dq4kwTdw.css +1 -0
- coord/dashboard/webapp/dist/assets/workbox-window.prod.es5-BqEJf4Xk.js +2 -0
- coord/dashboard/webapp/dist/icons/icon-192.png +0 -0
- coord/dashboard/webapp/dist/icons/icon-512.png +0 -0
- coord/dashboard/webapp/dist/icons/icon.svg +5 -0
- coord/dashboard/webapp/dist/index.html +38 -0
- coord/dashboard/webapp/dist/manifest.webmanifest +1 -0
- coord/dashboard/webapp/dist/sw.js +1 -0
- coord/dashboard/webapp/dist/workbox-e4022e15.js +1 -0
- coord/dashboard/webapp/e2e/available-gates-terminal.spec.ts +75 -0
- coord/dashboard/webapp/e2e/deep-link.spec.ts +172 -0
- coord/dashboard/webapp/e2e/fixtureServer.ts +155 -0
- coord/dashboard/webapp/e2e/live-update-fixture.spec.ts +113 -0
- coord/dashboard/webapp/e2e/realtime.spec.ts +238 -0
- coord/dashboard/webapp/e2e/shell.spec.ts +309 -0
- coord/dashboard/webapp/e2e/smoke.spec.ts +191 -0
- coord/dashboard/webapp/e2e/terminal.spec.ts +420 -0
- coord/dashboard/webapp/e2e/theme.spec.ts +138 -0
- coord/dashboard/webapp/eslint.config.js +20 -0
- coord/dashboard/webapp/index.html +37 -0
- coord/dashboard/webapp/node_modules/flatted/python/flatted.py +144 -0
- coord/dashboard/webapp/package-lock.json +10584 -0
- coord/dashboard/webapp/package.json +63 -0
- coord/dashboard/webapp/playwright.acceptance.config.ts +166 -0
- coord/dashboard/webapp/playwright.config.ts +93 -0
- coord/dashboard/webapp/postcss.config.js +6 -0
- coord/dashboard/webapp/public/icons/icon-192.png +0 -0
- coord/dashboard/webapp/public/icons/icon-512.png +0 -0
- coord/dashboard/webapp/public/icons/icon.svg +5 -0
- coord/dashboard/webapp/src/App.tsx +140 -0
- coord/dashboard/webapp/src/api/client.ts +199 -0
- coord/dashboard/webapp/src/api/generated.ts +176 -0
- coord/dashboard/webapp/src/components/ConnectionBadge.tsx +52 -0
- coord/dashboard/webapp/src/components/Detail.tsx +800 -0
- coord/dashboard/webapp/src/components/Gallery.tsx +341 -0
- coord/dashboard/webapp/src/components/Home.tsx +435 -0
- coord/dashboard/webapp/src/components/MobileKeyBar.tsx +280 -0
- coord/dashboard/webapp/src/components/PanelHeader.tsx +59 -0
- coord/dashboard/webapp/src/components/PipelineCard.tsx +168 -0
- coord/dashboard/webapp/src/components/SessionCard.tsx +99 -0
- coord/dashboard/webapp/src/components/SessionDetail.tsx +140 -0
- coord/dashboard/webapp/src/components/SessionsList.tsx +81 -0
- coord/dashboard/webapp/src/components/Terminal.tsx +376 -0
- coord/dashboard/webapp/src/components/__tests__/ConnectionBadge.test.tsx +81 -0
- coord/dashboard/webapp/src/components/__tests__/Detail.test.tsx +680 -0
- coord/dashboard/webapp/src/components/__tests__/Gallery.test.tsx +83 -0
- coord/dashboard/webapp/src/components/__tests__/Home.test.tsx +271 -0
- coord/dashboard/webapp/src/components/__tests__/MobileKeyBar.test.tsx +197 -0
- coord/dashboard/webapp/src/components/__tests__/PipelineCard.test.tsx +143 -0
- coord/dashboard/webapp/src/components/__tests__/SessionCard.test.tsx +106 -0
- coord/dashboard/webapp/src/components/__tests__/Terminal.test.tsx +504 -0
- coord/dashboard/webapp/src/components/ui/badge.tsx +41 -0
- coord/dashboard/webapp/src/components/ui/button.tsx +54 -0
- coord/dashboard/webapp/src/components/ui/card.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/dialog.tsx +99 -0
- coord/dashboard/webapp/src/components/ui/dropdown-menu.tsx +189 -0
- coord/dashboard/webapp/src/components/ui/empty-state.tsx +35 -0
- coord/dashboard/webapp/src/components/ui/sheet.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/skeleton.tsx +9 -0
- coord/dashboard/webapp/src/components/ui/tabs.tsx +55 -0
- coord/dashboard/webapp/src/components/ui/theme-provider.tsx +78 -0
- coord/dashboard/webapp/src/components/ui/theme-toggle.tsx +20 -0
- coord/dashboard/webapp/src/components/ui/toast.tsx +123 -0
- coord/dashboard/webapp/src/components/ui/toaster.tsx +30 -0
- coord/dashboard/webapp/src/components/ui/tooltip.tsx +26 -0
- coord/dashboard/webapp/src/components/ui/use-toast.ts +134 -0
- coord/dashboard/webapp/src/index.css +210 -0
- coord/dashboard/webapp/src/lib/pipeline.ts +29 -0
- coord/dashboard/webapp/src/lib/utils.ts +6 -0
- coord/dashboard/webapp/src/main.tsx +46 -0
- coord/dashboard/webapp/src/realtime/RealtimeProvider.tsx +112 -0
- coord/dashboard/webapp/src/realtime/__tests__/RealtimeProvider.test.tsx +189 -0
- coord/dashboard/webapp/src/realtime/__tests__/connection.test.ts +255 -0
- coord/dashboard/webapp/src/realtime/connection.ts +227 -0
- coord/dashboard/webapp/src/realtime/events.ts +100 -0
- coord/dashboard/webapp/src/routes/__tests__/paths.test.ts +92 -0
- coord/dashboard/webapp/src/routes/paths.ts +92 -0
- coord/dashboard/webapp/src/shell/ActivityRail.tsx +335 -0
- coord/dashboard/webapp/src/shell/AppShell.tsx +276 -0
- coord/dashboard/webapp/src/shell/ComingSoon.tsx +33 -0
- coord/dashboard/webapp/src/shell/EmptyDetail.tsx +26 -0
- coord/dashboard/webapp/src/shell/RouteNotFound.tsx +33 -0
- coord/dashboard/webapp/src/shell/ShellLayout.tsx +147 -0
- coord/dashboard/webapp/src/shell/StatusBar.tsx +46 -0
- coord/dashboard/webapp/src/shell/__tests__/ShellLayout.test.tsx +520 -0
- coord/dashboard/webapp/src/shell/__tests__/shellState.test.ts +95 -0
- coord/dashboard/webapp/src/shell/__tests__/stubViewport.ts +40 -0
- coord/dashboard/webapp/src/shell/breakpoints.ts +87 -0
- coord/dashboard/webapp/src/shell/railItems.ts +105 -0
- coord/dashboard/webapp/src/shell/shellState.ts +174 -0
- coord/dashboard/webapp/src/shell/useRegionFocus.ts +95 -0
- coord/dashboard/webapp/src/test-setup.ts +41 -0
- coord/dashboard/webapp/src/vite-env.d.ts +2 -0
- coord/dashboard/webapp/tailwind.config.js +140 -0
- coord/dashboard/webapp/tsconfig.json +25 -0
- coord/dashboard/webapp/tsconfig.node.json +11 -0
- coord/dashboard/webapp/vite.config.ts +71 -0
- coord/db.py +1076 -0
- coord/dead_end.py +332 -0
- coord/deploy/README.md +33 -0
- coord/deploy/coord-agent.service +89 -0
- coord/deploy/coord-db-backup.service +60 -0
- coord/deploy/coord-db-backup.sh +74 -0
- coord/deploy/coord-db-backup.timer +18 -0
- coord/deploy/coord-drive-queue.service +117 -0
- coord/deploy/coord-drive-queue.timer +39 -0
- coord/deploy/coord-notify.service +48 -0
- coord/deploy/coord-notify.timer +24 -0
- coord/deploy/coord-release-propagate.service +83 -0
- coord/deploy/coord-release-propagate.timer +38 -0
- coord/deploy/coord-release-window.service +119 -0
- coord/deploy/coord-release-window.timer +36 -0
- coord/deploy/coord-serve.service +82 -0
- coord/deploy/coord-web-dist-build.service +43 -0
- coord/deploy/coord-web-dist-build.timer +36 -0
- coord/deploy/coord-web.service +125 -0
- coord/deploy_manifest.py +80 -0
- coord/deploy_units.py +384 -0
- coord/deps.py +115 -0
- coord/diagnose.py +1623 -0
- coord/dispatch.py +1009 -0
- coord/dist_name.py +123 -0
- coord/drive.py +3101 -0
- coord/drive_queue.py +2298 -0
- coord/drive_state.py +870 -0
- coord/events.py +381 -0
- coord/failure_class.py +914 -0
- coord/filelock.py +168 -0
- coord/fleet_config_health.py +300 -0
- coord/freshness.py +206 -0
- coord/gate_a.py +469 -0
- coord/gate_b.py +411 -0
- coord/gate_snapshot.py +385 -0
- coord/gates.py +582 -0
- coord/github_ops.py +1954 -0
- coord/goal.py +125 -0
- coord/graph_health.py +348 -0
- coord/health/__init__.py +69 -0
- coord/health/aggregate.py +129 -0
- coord/health/checks/__init__.py +13 -0
- coord/health/checks/agent_install.py +280 -0
- coord/health/checks/cargo_targets.py +171 -0
- coord/health/checks/claude_binary.py +65 -0
- coord/health/checks/deploy_lane_facts.py +458 -0
- coord/health/checks/disk.py +99 -0
- coord/health/checks/fleet_board.py +89 -0
- coord/health/checks/fleet_deploy_lanes.py +469 -0
- coord/health/checks/fleet_phantom.py +69 -0
- coord/health/checks/fleet_unit_drift.py +151 -0
- coord/health/checks/graph.py +192 -0
- coord/health/checks/plan_usage.py +88 -0
- coord/health/checks/repo_state.py +161 -0
- coord/health/checks/spawned_coord.py +465 -0
- coord/health/checks/timer_active.py +254 -0
- coord/health/checks/toolchain.py +547 -0
- coord/health/checks/unit_drift.py +648 -0
- coord/health/checks/unit_enablement.py +171 -0
- coord/health/checks/worktrees.py +96 -0
- coord/health/cli.py +121 -0
- coord/health/context.py +106 -0
- coord/health/fleet_snapshot.py +477 -0
- coord/health/models.py +250 -0
- coord/health/pypi.py +231 -0
- coord/health/registry.py +240 -0
- coord/health/render.py +82 -0
- coord/health/units.py +60 -0
- coord/hooks.py +106 -0
- coord/housekeeping.py +204 -0
- coord/interactive.py +4286 -0
- coord/issue_store.py +1496 -0
- coord/liveness_auditor.py +293 -0
- coord/machine_pause.py +755 -0
- coord/merge_queue.py +4681 -0
- coord/milestone_chat.py +600 -0
- coord/milestone_dispatch.py +943 -0
- coord/milestone_gate.py +709 -0
- coord/milestone_order.py +840 -0
- coord/mock_author.py +334 -0
- coord/models.py +891 -0
- coord/network.py +269 -0
- coord/new_issue_chat.py +229 -0
- coord/notify.py +3226 -0
- coord/openapi.py +404 -0
- coord/overlap_fence.py +133 -0
- coord/parentage.py +200 -0
- coord/parentage_github.py +58 -0
- coord/pipeline.py +481 -0
- coord/plan_parser.py +266 -0
- coord/plans.py +543 -0
- coord/platform_paths.py +43 -0
- coord/pr_body_lint.py +67 -0
- coord/prereqs.py +533 -0
- coord/progress.py +425 -0
- coord/providers/__init__.py +683 -0
- coord/providers/base.py +218 -0
- coord/providers/claude.py +284 -0
- coord/providers/claude_pty.py +610 -0
- coord/providers/opencode.py +896 -0
- coord/reconcile.py +2233 -0
- coord/refine_chat.py +485 -0
- coord/release_cordon.py +525 -0
- coord/release_propagate.py +1176 -0
- coord/release_verify.py +777 -0
- coord/release_window.py +322 -0
- coord/reports.py +1643 -0
- coord/revalidate.py +1101 -0
- coord/review.py +3317 -0
- coord/scorecard.py +484 -0
- coord/serve_app.py +7192 -0
- coord/skills/update-issue/SKILL.md +93 -0
- coord/smoke.py +1030 -0
- coord/split_work.py +210 -0
- coord/stage_projection.py +650 -0
- coord/state.py +5720 -0
- coord/test_author.py +1064 -0
- coord/test_chat.py +352 -0
- coord/test_orchestrator.py +494 -0
- coord/test_report.py +178 -0
- coord/tui_release.py +271 -0
- coord/usage.py +753 -0
- coord/usage_limits.py +358 -0
- coord/usage_rollup.py +709 -0
- coord/worker_events.py +954 -0
coord/split_work.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Smart task splitting for large worker dispatches.
|
|
2
|
+
|
|
3
|
+
When a proposal touches more files than the configured threshold, this module
|
|
4
|
+
analyses the file list and splits the work into parallel/sequential chunks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from coord.config import DispatchConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class WorkChunk:
|
|
19
|
+
"""A chunk of work split from a larger plan."""
|
|
20
|
+
|
|
21
|
+
chunk_id: int
|
|
22
|
+
files: list[str]
|
|
23
|
+
briefing_fragment: str
|
|
24
|
+
depends_on: list[int] = field(default_factory=list) # chunk IDs this depends on
|
|
25
|
+
estimated_turns: int = 20
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Helpers
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def _is_test_file(f: str) -> bool:
|
|
33
|
+
"""Return True if the file is a test file."""
|
|
34
|
+
p = Path(f)
|
|
35
|
+
return (
|
|
36
|
+
len(p.parts) > 0 and p.parts[0] in ("tests", "test")
|
|
37
|
+
or p.name.startswith("test_")
|
|
38
|
+
or p.name.endswith("_test.py")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _subsystem(f: str) -> str:
|
|
43
|
+
"""Return the top-level directory of a file path, or '' for root-level files."""
|
|
44
|
+
parts = Path(f).parts
|
|
45
|
+
return parts[0] if len(parts) > 1 else ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _estimate_turns(files: list[str]) -> int:
|
|
49
|
+
"""Rough turn estimate: 15 turns per file, minimum 10."""
|
|
50
|
+
return max(10, len(files) * 15)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _describe_files(files: list[str]) -> str:
|
|
54
|
+
"""Short description of files for a briefing fragment."""
|
|
55
|
+
if not files:
|
|
56
|
+
return ""
|
|
57
|
+
if len(files) <= 3:
|
|
58
|
+
return ", ".join(files)
|
|
59
|
+
return f"{', '.join(files[:3])}, and {len(files) - 3} more"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _batch(items: list[str], size: int) -> list[list[str]]:
|
|
63
|
+
"""Split *items* into batches of at most *size* items."""
|
|
64
|
+
return [items[i : i + size] for i in range(0, len(items), size)]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _module_stem(f: str) -> str | None:
|
|
68
|
+
"""Return the bare module name (no test_ prefix, no .py suffix) for .py files."""
|
|
69
|
+
p = Path(f)
|
|
70
|
+
if p.suffix != ".py":
|
|
71
|
+
return None
|
|
72
|
+
stem = p.stem
|
|
73
|
+
if stem.startswith("test_"):
|
|
74
|
+
return stem[5:]
|
|
75
|
+
if stem.endswith("_test"):
|
|
76
|
+
return stem[:-5]
|
|
77
|
+
return stem
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Core entry point
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def analyze_plan(
|
|
85
|
+
files_likely: list[str],
|
|
86
|
+
dispatch_config: "DispatchConfig",
|
|
87
|
+
) -> list[WorkChunk]:
|
|
88
|
+
"""Analyse files and split into parallel/sequential chunks if needed.
|
|
89
|
+
|
|
90
|
+
Returns a single-chunk list (chunk_id=1) when the total file count is at or
|
|
91
|
+
below ``dispatch_config.max_files_per_worker``.
|
|
92
|
+
|
|
93
|
+
Splitting heuristics
|
|
94
|
+
--------------------
|
|
95
|
+
a. **File count**: more than ``max_files_per_worker`` triggers a split.
|
|
96
|
+
b. **Subsystem grouping**: files are grouped by their top-level directory.
|
|
97
|
+
Each subsystem becomes its own chunk (sub-split again if still too big).
|
|
98
|
+
c. **New module + consumers**: the impl chunk that introduces a new module
|
|
99
|
+
is labelled chunk A; test chunks that are named ``test_{module}`` are
|
|
100
|
+
given ``depends_on=[A.chunk_id]`` so they run after the module exists.
|
|
101
|
+
d. **Mechanical/cleanup chunk**: all test files (``tests/`` dir or
|
|
102
|
+
``test_*.py``) are batched into a single test chunk that depends on
|
|
103
|
+
every implementation chunk, or on the specific impl chunk it tests
|
|
104
|
+
(when the name matches ``test_{module}`` ↔ ``{module}.py``).
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
files_likely: List of file paths the worker will touch.
|
|
108
|
+
dispatch_config: Config section with splitting thresholds.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Ordered list of WorkChunk objects. Independent chunks can be
|
|
112
|
+
dispatched in parallel; chunks with ``depends_on`` must wait.
|
|
113
|
+
"""
|
|
114
|
+
if not files_likely or len(files_likely) <= dispatch_config.max_files_per_worker:
|
|
115
|
+
return [
|
|
116
|
+
WorkChunk(
|
|
117
|
+
chunk_id=1,
|
|
118
|
+
files=list(files_likely),
|
|
119
|
+
briefing_fragment=_describe_files(files_likely) if files_likely else "",
|
|
120
|
+
depends_on=[],
|
|
121
|
+
estimated_turns=_estimate_turns(files_likely),
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
# ── Separate tests from implementation files ──────────────────────────
|
|
126
|
+
test_files = [f for f in files_likely if _is_test_file(f)]
|
|
127
|
+
impl_files = [f for f in files_likely if not _is_test_file(f)]
|
|
128
|
+
|
|
129
|
+
# ── Group impl files by top-level directory (subsystem) ───────────────
|
|
130
|
+
groups: dict[str, list[str]] = {}
|
|
131
|
+
for f in impl_files:
|
|
132
|
+
key = _subsystem(f)
|
|
133
|
+
groups.setdefault(key, []).append(f)
|
|
134
|
+
|
|
135
|
+
# ── Create one chunk per subsystem (sub-split if still oversized) ─────
|
|
136
|
+
chunks: list[WorkChunk] = []
|
|
137
|
+
chunk_id = 1
|
|
138
|
+
|
|
139
|
+
for subsystem in sorted(groups.keys()):
|
|
140
|
+
sub_files = groups[subsystem]
|
|
141
|
+
label = subsystem if subsystem else "root"
|
|
142
|
+
for batch in _batch(sub_files, dispatch_config.max_files_per_worker):
|
|
143
|
+
frag = f"{label}: {_describe_files(batch)}"
|
|
144
|
+
chunks.append(
|
|
145
|
+
WorkChunk(
|
|
146
|
+
chunk_id=chunk_id,
|
|
147
|
+
files=batch,
|
|
148
|
+
briefing_fragment=frag,
|
|
149
|
+
depends_on=[],
|
|
150
|
+
estimated_turns=_estimate_turns(batch),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
chunk_id += 1
|
|
154
|
+
|
|
155
|
+
# ── Build module-name → chunk-id map for dependency wiring ───────────
|
|
156
|
+
# Maps bare stem (e.g. "split_work") → chunk_id of the impl chunk that
|
|
157
|
+
# introduces it. We prefer the *first* chunk that owns the file.
|
|
158
|
+
module_to_chunk: dict[str, int] = {}
|
|
159
|
+
for chunk in chunks:
|
|
160
|
+
for f in chunk.files:
|
|
161
|
+
stem = _module_stem(f)
|
|
162
|
+
if stem and stem not in module_to_chunk:
|
|
163
|
+
module_to_chunk[stem] = chunk.chunk_id
|
|
164
|
+
|
|
165
|
+
# ── Add test chunk (if any test files exist) ──────────────────────────
|
|
166
|
+
if test_files:
|
|
167
|
+
test_deps: set[int] = set()
|
|
168
|
+
has_unmatched = False
|
|
169
|
+
for tf in test_files:
|
|
170
|
+
stem = _module_stem(tf)
|
|
171
|
+
if stem and stem in module_to_chunk:
|
|
172
|
+
# This test is specifically for a new module in chunk N
|
|
173
|
+
test_deps.add(module_to_chunk[stem])
|
|
174
|
+
else:
|
|
175
|
+
has_unmatched = True
|
|
176
|
+
|
|
177
|
+
if not test_deps or has_unmatched:
|
|
178
|
+
# Some test files have no name-based match → conservatively depend
|
|
179
|
+
# on ALL implementation chunks to guarantee ordering correctness.
|
|
180
|
+
test_deps = {c.chunk_id for c in chunks}
|
|
181
|
+
|
|
182
|
+
test_label = _subsystem(test_files[0]) or "tests"
|
|
183
|
+
frag = f"{test_label}: {_describe_files(test_files)}"
|
|
184
|
+
chunks.append(
|
|
185
|
+
WorkChunk(
|
|
186
|
+
chunk_id=chunk_id,
|
|
187
|
+
files=test_files,
|
|
188
|
+
briefing_fragment=frag,
|
|
189
|
+
depends_on=sorted(test_deps),
|
|
190
|
+
estimated_turns=_estimate_turns(test_files),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return chunks
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def format_chunks_summary(chunks: list[WorkChunk]) -> str:
|
|
198
|
+
"""Return a human-readable summary of the split chunks."""
|
|
199
|
+
if len(chunks) <= 1:
|
|
200
|
+
return ""
|
|
201
|
+
lines: list[str] = [f" Split into {len(chunks)} chunks:"]
|
|
202
|
+
for chunk in chunks:
|
|
203
|
+
dep_str = ""
|
|
204
|
+
if chunk.depends_on:
|
|
205
|
+
dep_str = f" (after chunk {', '.join(str(d) for d in chunk.depends_on)})"
|
|
206
|
+
lines.append(
|
|
207
|
+
f" Chunk {chunk.chunk_id}: {len(chunk.files)} files — "
|
|
208
|
+
f"{chunk.briefing_fragment}{dep_str}"
|
|
209
|
+
)
|
|
210
|
+
return "\n".join(lines)
|