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/plan_parser.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Parse structured plan output from plan-only worker logs.
|
|
2
|
+
|
|
3
|
+
Plan workers (``type="plan"`` assignments) are instructed to output a fixed
|
|
4
|
+
set of section headings. This module extracts those headings from either a
|
|
5
|
+
plain-text log or a stream-json log and returns a :class:`WorkerPlan`
|
|
6
|
+
dataclass.
|
|
7
|
+
|
|
8
|
+
Recognised headings (as defined in ``WORKER_PLAN_PROMPT`` in ``coord.agent``):
|
|
9
|
+
|
|
10
|
+
PLAN: (optional) short one-line summary of the overall plan
|
|
11
|
+
FILES_READ: comma-separated list of every file the worker examined
|
|
12
|
+
FILES_MODIFY: comma-separated list of files that would need to change
|
|
13
|
+
APPROACH: concise implementation approach (multi-sentence)
|
|
14
|
+
RISKS: potential blockers, conflicts, or tricky areas
|
|
15
|
+
ESTIMATE: rough complexity: trivial | small | medium | large
|
|
16
|
+
|
|
17
|
+
Plus a fenced ``SMOKE_TESTS:`` / ``END_SMOKE_TESTS`` block — bullets the
|
|
18
|
+
human should run after the work lands. Parsed the same way the work
|
|
19
|
+
worker's end-of-job block is (``coord.progress.parse_smoke_tests``) so
|
|
20
|
+
the two sources are interchangeable.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# All section keywords the parser recognises. Order matters for display but
|
|
31
|
+
# not for parsing.
|
|
32
|
+
SECTION_KEYWORDS = ["PLAN", "FILES_READ", "FILES_MODIFY", "APPROACH", "RISKS", "ESTIMATE"]
|
|
33
|
+
|
|
34
|
+
# Match a section header at the start of a line, capturing the keyword.
|
|
35
|
+
# Tolerates optional leading markdown (e.g. "### FILES_READ:" or "**FILES_READ:**").
|
|
36
|
+
# The trailing optional \*{1,2} handles bold-close after the colon: **KEYWORD:**
|
|
37
|
+
_SECTION_RE = re.compile(
|
|
38
|
+
r"^(?:[#*]+\s+|\*{1,2})?(" + "|".join(SECTION_KEYWORDS) + r"):(?:\*{1,2})?\s*",
|
|
39
|
+
re.MULTILINE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class WorkerPlan:
|
|
45
|
+
"""Structured plan extracted from a plan-only worker log."""
|
|
46
|
+
|
|
47
|
+
plan: str = ""
|
|
48
|
+
files_read: list[str] = field(default_factory=list)
|
|
49
|
+
files_modify: list[str] = field(default_factory=list)
|
|
50
|
+
approach: str = ""
|
|
51
|
+
risks: str = ""
|
|
52
|
+
estimate: str = ""
|
|
53
|
+
# SMOKE_TESTS block emitted by the plan worker. Tri-state, same shape as
|
|
54
|
+
# Assignment.smoke_tests / parse_smoke_tests_from_log:
|
|
55
|
+
# None — no block emitted (or block was empty/malformed)
|
|
56
|
+
# [] — explicit "(none — change is internal)"
|
|
57
|
+
# list — one bullet per item
|
|
58
|
+
smoke_tests: list[str] | None = None
|
|
59
|
+
raw_text: str = ""
|
|
60
|
+
|
|
61
|
+
def is_empty(self) -> bool:
|
|
62
|
+
"""True iff no structured sections were found."""
|
|
63
|
+
return not any(
|
|
64
|
+
[
|
|
65
|
+
self.plan,
|
|
66
|
+
self.files_read,
|
|
67
|
+
self.files_modify,
|
|
68
|
+
self.approach,
|
|
69
|
+
self.risks,
|
|
70
|
+
self.estimate,
|
|
71
|
+
self.smoke_tests is not None,
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> dict:
|
|
76
|
+
return {
|
|
77
|
+
"plan": self.plan,
|
|
78
|
+
"files_read": self.files_read,
|
|
79
|
+
"files_modify": self.files_modify,
|
|
80
|
+
"approach": self.approach,
|
|
81
|
+
"risks": self.risks,
|
|
82
|
+
"estimate": self.estimate,
|
|
83
|
+
"smoke_tests": self.smoke_tests,
|
|
84
|
+
"raw_text": self.raw_text,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_dict(cls, data: dict) -> WorkerPlan:
|
|
89
|
+
return cls(
|
|
90
|
+
plan=data.get("plan", ""),
|
|
91
|
+
files_read=data.get("files_read", []),
|
|
92
|
+
files_modify=data.get("files_modify", []),
|
|
93
|
+
approach=data.get("approach", ""),
|
|
94
|
+
risks=data.get("risks", ""),
|
|
95
|
+
estimate=data.get("estimate", ""),
|
|
96
|
+
smoke_tests=data.get("smoke_tests"),
|
|
97
|
+
raw_text=data.get("raw_text", ""),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ── Text extraction ──────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _extract_text_from_stream_json(log_path: Path) -> str:
|
|
105
|
+
"""Concatenate all assistant text blocks from a stream-json log.
|
|
106
|
+
|
|
107
|
+
#1710 inventory: kept as a direct ``coord.worker_events`` import rather
|
|
108
|
+
than routed through ``provider.parse_log()`` — this decodes the generic
|
|
109
|
+
Anthropic-Messages-API ``type: "assistant"`` / ``message.content``
|
|
110
|
+
envelope (a wire-format detail, not claude business semantics), and
|
|
111
|
+
``WorkerSummary`` has no "raw assistant text" field to route through
|
|
112
|
+
without adding a new abstract ``Provider`` method (which would force a
|
|
113
|
+
change in every concrete provider, including the out-of-scope
|
|
114
|
+
``opencode.py``). Same reasoning as ``coord.review``'s equivalent
|
|
115
|
+
helpers; see ``tests/test_provider_seam.py``.
|
|
116
|
+
"""
|
|
117
|
+
from coord.worker_events import _assistant_text, parse_event # noqa: PLC0415
|
|
118
|
+
|
|
119
|
+
parts: list[str] = []
|
|
120
|
+
try:
|
|
121
|
+
with open(log_path, encoding="utf-8", errors="replace") as f:
|
|
122
|
+
for line in f:
|
|
123
|
+
event = parse_event(line.rstrip("\n"))
|
|
124
|
+
if event is None:
|
|
125
|
+
continue
|
|
126
|
+
if event.type == "assistant":
|
|
127
|
+
text = _assistant_text(event)
|
|
128
|
+
if text:
|
|
129
|
+
parts.append(text)
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
return "\n".join(parts)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ── Section parsing ──────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_sections(text: str) -> dict[str, str]:
|
|
139
|
+
"""Split *text* on section headers and return a keyword → content dict."""
|
|
140
|
+
if not text:
|
|
141
|
+
return {}
|
|
142
|
+
matches = list(_SECTION_RE.finditer(text))
|
|
143
|
+
if not matches:
|
|
144
|
+
return {}
|
|
145
|
+
result: dict[str, str] = {}
|
|
146
|
+
for i, m in enumerate(matches):
|
|
147
|
+
keyword = m.group(1)
|
|
148
|
+
content_start = m.end()
|
|
149
|
+
content_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
150
|
+
result[keyword] = text[content_start:content_end].strip()
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _parse_file_list(value: str) -> list[str]:
|
|
155
|
+
"""Split a comma-separated file list, stripping blank entries."""
|
|
156
|
+
if not value:
|
|
157
|
+
return []
|
|
158
|
+
return [f.strip() for f in value.split(",") if f.strip()]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ── Public API ───────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def parse_plan_text(text: str) -> WorkerPlan:
|
|
165
|
+
"""Parse structured plan sections from raw text.
|
|
166
|
+
|
|
167
|
+
Returns a :class:`WorkerPlan` whose ``raw_text`` is always set to *text*,
|
|
168
|
+
and whose section fields are populated from any recognised headings found.
|
|
169
|
+
"""
|
|
170
|
+
from coord.progress import _extract_smoke_tests_from_text # noqa: PLC0415
|
|
171
|
+
|
|
172
|
+
sections = _parse_sections(text)
|
|
173
|
+
return WorkerPlan(
|
|
174
|
+
plan=sections.get("PLAN", ""),
|
|
175
|
+
files_read=_parse_file_list(sections.get("FILES_READ", "")),
|
|
176
|
+
files_modify=_parse_file_list(sections.get("FILES_MODIFY", "")),
|
|
177
|
+
approach=sections.get("APPROACH", ""),
|
|
178
|
+
risks=sections.get("RISKS", ""),
|
|
179
|
+
estimate=sections.get("ESTIMATE", ""),
|
|
180
|
+
smoke_tests=_extract_smoke_tests_from_text(text),
|
|
181
|
+
raw_text=text,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def parse_plan_from_log(log_path: str | Path) -> WorkerPlan | None:
|
|
186
|
+
"""Parse a :class:`WorkerPlan` from a worker log file.
|
|
187
|
+
|
|
188
|
+
Handles both stream-json (``--output-format stream-json``) and plain-text
|
|
189
|
+
log formats. Returns ``None`` if the file does not exist or contains no
|
|
190
|
+
recognised plan sections.
|
|
191
|
+
|
|
192
|
+
#1710 inventory: ``is_stream_json`` is a generic on-disk-shape sniff, not
|
|
193
|
+
claude-specific parsing — kept direct. See the note on
|
|
194
|
+
``_extract_text_from_stream_json`` above.
|
|
195
|
+
"""
|
|
196
|
+
from coord.worker_events import is_stream_json # noqa: PLC0415
|
|
197
|
+
|
|
198
|
+
p = Path(log_path)
|
|
199
|
+
if not p.exists():
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
if is_stream_json(p):
|
|
203
|
+
text = _extract_text_from_stream_json(p)
|
|
204
|
+
else:
|
|
205
|
+
try:
|
|
206
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
207
|
+
except OSError:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
plan = parse_plan_text(text)
|
|
211
|
+
return None if plan.is_empty() else plan
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def parse_plan_from_agent(
|
|
215
|
+
host: str,
|
|
216
|
+
assignment_id: str,
|
|
217
|
+
port: int = 7433,
|
|
218
|
+
timeout: float = 15.0,
|
|
219
|
+
) -> WorkerPlan | None:
|
|
220
|
+
"""Fetch a plan worker's log via the agent's ``/logs/<id>`` endpoint
|
|
221
|
+
and parse it.
|
|
222
|
+
|
|
223
|
+
Use this instead of :func:`parse_plan_from_log` when the plan worker
|
|
224
|
+
ran on a remote agent and the log file isn't on the coordinator's
|
|
225
|
+
local filesystem. Mirrors :func:`coord.review.parse_review_from_agent`.
|
|
226
|
+
Returns ``None`` on network failure, empty log, or no recognised plan
|
|
227
|
+
sections.
|
|
228
|
+
"""
|
|
229
|
+
import httpx # noqa: PLC0415
|
|
230
|
+
|
|
231
|
+
url = f"http://{host}:{port}/logs/{assignment_id}"
|
|
232
|
+
try:
|
|
233
|
+
resp = httpx.get(url, timeout=timeout)
|
|
234
|
+
resp.raise_for_status()
|
|
235
|
+
text = resp.text
|
|
236
|
+
except (httpx.HTTPError, httpx.TimeoutException):
|
|
237
|
+
return None
|
|
238
|
+
if not text:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
# The agent returns stream-json verbatim when the worker was launched
|
|
242
|
+
# with --output-format stream-json (the standard). Detect the same way
|
|
243
|
+
# is_stream_json() does for files: first non-comment, non-blank line
|
|
244
|
+
# starts with `{`.
|
|
245
|
+
stream_json = False
|
|
246
|
+
for line in text.splitlines():
|
|
247
|
+
stripped = line.strip()
|
|
248
|
+
if not stripped or stripped.startswith("#"):
|
|
249
|
+
continue
|
|
250
|
+
stream_json = stripped.startswith("{")
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if stream_json:
|
|
254
|
+
# Reuse the stream-json text extractor by writing to a temp file —
|
|
255
|
+
# the existing extractor opens by path. Cheap enough at log sizes
|
|
256
|
+
# we see in practice (≤ a few MB).
|
|
257
|
+
import tempfile # noqa: PLC0415
|
|
258
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=True) as tf:
|
|
259
|
+
tf.write(text)
|
|
260
|
+
tf.flush()
|
|
261
|
+
extracted = _extract_text_from_stream_json(Path(tf.name))
|
|
262
|
+
else:
|
|
263
|
+
extracted = text
|
|
264
|
+
|
|
265
|
+
plan = parse_plan_text(extracted)
|
|
266
|
+
return None if plan.is_empty() else plan
|