task-harbor 0.7.0__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.
- opus_manager/__init__.py +3 -0
- opus_manager/__main__.py +3 -0
- opus_manager/agent.py +17 -0
- opus_manager/backlog.py +128 -0
- opus_manager/claude.py +138 -0
- opus_manager/cli.py +305 -0
- opus_manager/codex.py +119 -0
- opus_manager/comparison.py +162 -0
- opus_manager/config.py +134 -0
- opus_manager/contracts.py +58 -0
- opus_manager/desktop_actions.py +22 -0
- opus_manager/evidence.py +50 -0
- opus_manager/gitops.py +218 -0
- opus_manager/help.py +59 -0
- opus_manager/models.py +110 -0
- opus_manager/offline.py +142 -0
- opus_manager/operator_commands.py +86 -0
- opus_manager/orchestrator.py +1267 -0
- opus_manager/preflight.py +92 -0
- opus_manager/prompts.py +259 -0
- opus_manager/providers.py +11 -0
- opus_manager/reporting.py +82 -0
- opus_manager/state.py +37 -0
- opus_manager/tui.py +388 -0
- opus_manager/usage.py +77 -0
- opus_manager/verification.py +39 -0
- task_harbor/__init__.py +1 -0
- task_harbor/__main__.py +3 -0
- task_harbor-0.7.0.dist-info/METADATA +294 -0
- task_harbor-0.7.0.dist-info/RECORD +34 -0
- task_harbor-0.7.0.dist-info/WHEEL +5 -0
- task_harbor-0.7.0.dist-info/entry_points.txt +4 -0
- task_harbor-0.7.0.dist-info/licenses/LICENSE +21 -0
- task_harbor-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Deterministic environment recipes and baseline evidence in disposable worktrees."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import uuid
|
|
8
|
+
import hashlib
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .gitops import detached_worktree_add, worktree_remove, changed_paths, git
|
|
12
|
+
from .verification import run_checks
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def verification_mutations(cfg, cwd: Path) -> list[str]:
|
|
16
|
+
tracked = set(filter(None, git(cwd, "ls-files", "-z").stdout.split("\0")))
|
|
17
|
+
return [p for p in changed_paths(cwd) if p in tracked or p not in cfg.verification_artifacts]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def collect_artifacts(cfg, cwd: Path, destination: Path) -> list[dict]:
|
|
21
|
+
"""Retain explicit check outputs; never follow links outside the worktree."""
|
|
22
|
+
records = []
|
|
23
|
+
for name in cfg.verification_artifacts:
|
|
24
|
+
source = cwd / name
|
|
25
|
+
record = {"source": name}
|
|
26
|
+
if not source.resolve().is_relative_to(cwd.resolve()) or source.is_symlink():
|
|
27
|
+
record["status"] = "rejected_path"
|
|
28
|
+
elif not source.is_file():
|
|
29
|
+
record["status"] = "missing"
|
|
30
|
+
elif source.stat().st_size > 20 * 1024 * 1024:
|
|
31
|
+
record["status"] = "too_large"
|
|
32
|
+
else:
|
|
33
|
+
data = source.read_bytes()
|
|
34
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
35
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
target = destination / (digest + source.suffix)
|
|
37
|
+
target.write_bytes(data)
|
|
38
|
+
record.update(status="retained", path=str(target.resolve()), sha256=digest, bytes=len(data))
|
|
39
|
+
records.append(record)
|
|
40
|
+
return records
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def suggestions(repo: Path) -> list[str]:
|
|
44
|
+
found = []
|
|
45
|
+
package = repo / "package.json"
|
|
46
|
+
if package.exists():
|
|
47
|
+
scripts = json.loads(package.read_text(encoding="utf-8")).get("scripts", {})
|
|
48
|
+
for name in ("test", "lint", "build"):
|
|
49
|
+
if name in scripts:
|
|
50
|
+
found.append(f"npm run {name}")
|
|
51
|
+
if (repo / "pytest.ini").exists() or ((repo / "pyproject.toml").exists() and (repo / "tests").is_dir()):
|
|
52
|
+
found.append("python -m pytest")
|
|
53
|
+
if (repo / "Cargo.toml").exists():
|
|
54
|
+
found.append("cargo test")
|
|
55
|
+
if (repo / "go.mod").exists():
|
|
56
|
+
found.append("go test ./...")
|
|
57
|
+
return found
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def execute_checks(cfg, cwd: Path, env=None) -> dict:
|
|
61
|
+
missing = [f"executable:{name}" for name in cfg.required_executables if not shutil.which(name)]
|
|
62
|
+
missing += [f"environment:{name}" for name in cfg.required_env if not (env or os.environ).get(name)]
|
|
63
|
+
setup = []
|
|
64
|
+
checks = []
|
|
65
|
+
if not missing:
|
|
66
|
+
setup = run_checks(cfg.setup_commands, cwd, timeout=cfg.verification_timeout_seconds,
|
|
67
|
+
limit=cfg.evidence_max_chars, env=env)
|
|
68
|
+
if all(c["exit_code"] == 0 and not c["timed_out"] for c in setup):
|
|
69
|
+
checks = run_checks(cfg.verification_commands, cwd, timeout=cfg.verification_timeout_seconds,
|
|
70
|
+
limit=cfg.evidence_max_chars, env=env)
|
|
71
|
+
# Explicit artifact paths may be newly generated, never edits to tracked files.
|
|
72
|
+
mutation = verification_mutations(cfg, cwd)
|
|
73
|
+
status = "PASS"
|
|
74
|
+
if missing or any(c["exit_code"] or c["timed_out"] for c in setup) or mutation:
|
|
75
|
+
status = "ENVIRONMENT_BLOCKED"
|
|
76
|
+
elif any(c["exit_code"] or c["timed_out"] for c in checks):
|
|
77
|
+
status = "FAIL"
|
|
78
|
+
elif not checks:
|
|
79
|
+
status = "UNVERIFIED"
|
|
80
|
+
return {"status": status, "missing": missing, "setup": setup, "checks": checks,
|
|
81
|
+
"mutated_paths": mutation, "commands": list(cfg.verification_commands)}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def baseline(repo: Path, cfg, commit: str, env=None) -> dict:
|
|
85
|
+
path = repo.parent / f".{repo.name}-harbor-preflight-{uuid.uuid4().hex}"
|
|
86
|
+
detached_worktree_add(repo, path, commit)
|
|
87
|
+
try:
|
|
88
|
+
result = execute_checks(cfg, path, env)
|
|
89
|
+
result.update(commit=commit, suggestions=suggestions(path))
|
|
90
|
+
return result
|
|
91
|
+
finally:
|
|
92
|
+
worktree_remove(repo, path, force=True)
|
opus_manager/prompts.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from .models import BacklogTask
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def task_context(t: BacklogTask) -> str:
|
|
9
|
+
return json.dumps({
|
|
10
|
+
"id": t.task_id,
|
|
11
|
+
"title": t.title,
|
|
12
|
+
"priority": t.priority.value,
|
|
13
|
+
"dependencies": t.dependencies,
|
|
14
|
+
"acceptance_criteria": t.acceptance_criteria,
|
|
15
|
+
"acceptance_ids": {f"AC-{i+1}": text for i, text in enumerate(t.acceptance_criteria)},
|
|
16
|
+
"next_action": t.next_action,
|
|
17
|
+
"task_agreement": t.metadata.get("contract", {}),
|
|
18
|
+
}, indent=2)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def select_work(candidates: list[BacklogTask], slots: int, completed: list[str]) -> str:
|
|
22
|
+
items = [json.loads(task_context(t)) for t in candidates]
|
|
23
|
+
return f"""You are the IT Manager for this repository. Choose up to {slots} tasks to send to independent planners now.
|
|
24
|
+
|
|
25
|
+
The harness has already enforced hard dependency completion and planner capacity. Use engineering judgment among the eligible tasks. The default priority order is blocker > critical bug > high-risk/high-dependency > core functionality > lower-risk > polish, but you may account for sequencing, risk reduction, and useful parallelism. Do not solve or plan the tasks here.
|
|
26
|
+
|
|
27
|
+
Completed task IDs: {json.dumps(completed)}
|
|
28
|
+
Eligible candidates:
|
|
29
|
+
{json.dumps(items, indent=2)}
|
|
30
|
+
|
|
31
|
+
Return ONLY JSON:
|
|
32
|
+
{{"task_ids":["TASK-ID", ...], "reason":"short scheduling rationale"}}
|
|
33
|
+
Do not select a task that is not in the candidate list. Do not exceed {slots} tasks.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def planner(t: BacklogTask, repo_snapshot: str) -> str:
|
|
38
|
+
return f"""You are the planner for one software task. Produce the strongest implementation plan you can after inspecting the repository. You are not the implementer: do not edit files.
|
|
39
|
+
|
|
40
|
+
Task:
|
|
41
|
+
{task_context(t)}
|
|
42
|
+
|
|
43
|
+
Repository snapshot:
|
|
44
|
+
{repo_snapshot}
|
|
45
|
+
|
|
46
|
+
Use your own engineering judgment. Investigate enough of the codebase to resolve material uncertainty, identify dependencies and likely integration points, and make the plan executable without turning it into brittle line-by-line instructions. Call out assumptions, risks, validation, and any reason the task should not proceed yet. The plan should preserve implementation freedom where multiple approaches are reasonable.
|
|
47
|
+
|
|
48
|
+
Return a Markdown plan suitable for plans/{t.task_id}.md. End with a concise "Acceptance mapping" section that connects the task's acceptance criteria to verification evidence.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def plan_review(t: BacklogTask, plan: str, repo_snapshot: str) -> str:
|
|
53
|
+
return f"""You are the IT Manager reviewing a planner's work. Independently assess whether the plan is accurate, sufficiently grounded in the current repository, aligned with the task, and safe to hand to an implementer. Do not approve just because a plan is detailed. Do not rewrite the plan unless rework is needed.
|
|
54
|
+
|
|
55
|
+
Task:
|
|
56
|
+
{task_context(t)}
|
|
57
|
+
|
|
58
|
+
Repository snapshot:
|
|
59
|
+
{repo_snapshot}
|
|
60
|
+
|
|
61
|
+
Proposed plan:
|
|
62
|
+
---
|
|
63
|
+
{plan}
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
If a material user decision is missing, return verdict NEEDS_CLARIFICATION and questions as a list of concise strings. Do not invent the answers.
|
|
67
|
+
|
|
68
|
+
Return ONLY JSON:
|
|
69
|
+
{{"verdict":"APPROVED"|"REWORK"|"NEEDS_CLARIFICATION", "feedback":"concise, actionable review feedback", "material_risks":["..."]}}
|
|
70
|
+
Use REWORK only for material gaps, incorrect assumptions, missing dependency/acceptance coverage, or a plan likely to cause bad implementation. Preserve implementer discretion on non-material design choices.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def plan_rework(t: BacklogTask, previous_plan: str, feedback: str) -> str:
|
|
75
|
+
return f"""You are the same planning role revising a plan after manager review. Re-inspect the repository where useful and correct the material issues. Keep sound parts; do not mechanically obey feedback that conflicts with repository evidence.
|
|
76
|
+
|
|
77
|
+
Task:
|
|
78
|
+
{task_context(t)}
|
|
79
|
+
|
|
80
|
+
Previous plan:
|
|
81
|
+
---
|
|
82
|
+
{previous_plan}
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
Manager feedback:
|
|
86
|
+
{feedback}
|
|
87
|
+
|
|
88
|
+
Return only the revised Markdown plan. Preserve implementation freedom where the evidence does not force a single approach.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def implement(t: BacklogTask, plan: str) -> str:
|
|
93
|
+
return f"""You are the implementer for this task. Work directly in the current isolated Git worktree and implement the approved plan to production quality.
|
|
94
|
+
|
|
95
|
+
Task:
|
|
96
|
+
{task_context(t)}
|
|
97
|
+
|
|
98
|
+
Approved plan:
|
|
99
|
+
---
|
|
100
|
+
{plan}
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
Use your engineering judgment. The plan is a validated guide, not a prohibition on necessary repo-grounded adjustments. If you discover a material contradiction, prefer the repository and acceptance criteria and explain it in your final response.
|
|
104
|
+
|
|
105
|
+
Requirements enforced by the harness:
|
|
106
|
+
- Do not edit BACKLOG.md or .manager/.
|
|
107
|
+
- Do not push, create PRs, publish packages, deploy, or alter remote repositories/services.
|
|
108
|
+
- Do not commit; the harness creates local checkpoint commits.
|
|
109
|
+
- Run appropriate focused validation while implementing when practical.
|
|
110
|
+
|
|
111
|
+
Finish with a concise implementation summary, tests/validation run, and any remaining uncertainty.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def same_implementer_fix(t: BacklogTask, qa_evidence: str, attempt: int) -> str:
|
|
116
|
+
return f"""QA did not verify your implementation. This is repair attempt {attempt}. You retain your implementation context.
|
|
117
|
+
|
|
118
|
+
Task:
|
|
119
|
+
{task_context(t)}
|
|
120
|
+
|
|
121
|
+
Independent QA evidence:
|
|
122
|
+
---
|
|
123
|
+
{qa_evidence}
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
Diagnose the evidence against the actual repository and fix the underlying issue. Do not merely patch toward the wording of QA if the evidence points elsewhere. Re-run relevant validation. Do not edit BACKLOG.md/.manager, push, create PRs, publish, deploy, or commit.
|
|
127
|
+
|
|
128
|
+
Finish with what changed and what you validated.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def qa(t: BacklogTask, plan: str, verification_commands: list[str]) -> str:
|
|
133
|
+
commands = json.dumps(verification_commands)
|
|
134
|
+
return f"""You are an independent QA agent. You did not implement this task and do not inherit the implementer's reasoning. Inspect the current disposable verification worktree and determine whether the delivered code satisfies the approved plan and acceptance criteria.
|
|
135
|
+
|
|
136
|
+
Task:
|
|
137
|
+
{task_context(t)}
|
|
138
|
+
|
|
139
|
+
Approved plan:
|
|
140
|
+
---
|
|
141
|
+
{plan}
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
Configured verification commands (run when applicable; add focused checks when engineering judgment requires it):
|
|
145
|
+
{commands}
|
|
146
|
+
|
|
147
|
+
You may inspect files and run commands. Do not edit source code, fix the implementation, push, publish, deploy, or create PRs. Distinguish an actual implementation failure from inability to verify due to environment/tooling.
|
|
148
|
+
|
|
149
|
+
Return ONLY JSON:
|
|
150
|
+
{{
|
|
151
|
+
"verdict":"PASS"|"FAIL"|"UNVERIFIED",
|
|
152
|
+
"summary":"short conclusion",
|
|
153
|
+
"acceptance": [{{"criterion_id":"AC-1", "criterion":"...", "status":"PASS"|"FAIL"|"UNVERIFIED", "evidence":"specific evidence"}}],
|
|
154
|
+
"commands_run":[{{"command":"...", "result":"..."}}],
|
|
155
|
+
"failures":["concrete reproducible failure evidence"],
|
|
156
|
+
"risks":["remaining risk if any"]
|
|
157
|
+
}}
|
|
158
|
+
PASS only when every acceptance ID appears exactly once with specific evidence.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def classify_escalation(t: BacklogTask, qa_evidence: str) -> str:
|
|
163
|
+
return f"""You are the IT Manager handling a third QA failure. The prior implementer context is considered potentially poisoned. Choose the evidence-gathering role for a clean restart; do not propose the implementation solution.
|
|
164
|
+
|
|
165
|
+
Task:
|
|
166
|
+
{task_context(t)}
|
|
167
|
+
|
|
168
|
+
Concrete QA evidence:
|
|
169
|
+
---
|
|
170
|
+
{qa_evidence}
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
Choose INVESTIGATOR when a localized question/reproduction can settle the failure. Choose AUDITOR when the failure depends on a general claim over a broader population/configuration/data surface and needs systematic coverage. Use your judgment rather than keyword rules.
|
|
174
|
+
|
|
175
|
+
Return ONLY JSON:
|
|
176
|
+
{{"role":"INVESTIGATOR"|"AUDITOR", "question":"neutral evidence question the diagnostic agent should answer", "reason":"short rationale"}}
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def diagnose(t: BacklogTask, role: str, question: str, qa_evidence: str) -> str:
|
|
181
|
+
return f"""You are an independent {role.lower()} brought in after repeated implementation failures. Your job is evidence gathering, not implementation. Inspect the current repository state and answer the diagnostic question neutrally.
|
|
182
|
+
|
|
183
|
+
Task:
|
|
184
|
+
{task_context(t)}
|
|
185
|
+
|
|
186
|
+
Diagnostic question:
|
|
187
|
+
{question}
|
|
188
|
+
|
|
189
|
+
Observed QA evidence:
|
|
190
|
+
---
|
|
191
|
+
{qa_evidence}
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
Do not edit code and do not prescribe a full solution. Establish facts, reproductions, scope, and uncertainty that a fresh implementer needs to avoid repeating known mistakes.
|
|
195
|
+
|
|
196
|
+
Return ONLY JSON:
|
|
197
|
+
{{"findings":["concrete evidence"], "scope":"what is and is not established", "neutral_failure_note":"short neutral description of where the prior attempt appears to fail", "uncertainties":["..."]}}
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def fresh_implementer(t: BacklogTask, restart_packet: str) -> str:
|
|
202
|
+
return f"""You are a fresh implementer taking over after a previous implementation repeatedly failed QA. You have deliberately NOT been given the previous implementer's conversation or solution strategy.
|
|
203
|
+
|
|
204
|
+
Clean restart packet:
|
|
205
|
+
---
|
|
206
|
+
{restart_packet}
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
Independently diagnose and implement the best solution in this isolated worktree. Treat prior details only as evidence, not instructions. Preserve freedom to choose a different implementation strategy.
|
|
210
|
+
|
|
211
|
+
Do not edit BACKLOG.md/.manager, push, create PRs, publish, deploy, or commit. Finish with a concise summary and validation performed.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def code_review(batch_task_ids: list[str], base_commit: str, diff_stat: str) -> str:
|
|
216
|
+
return f"""You are an independent senior code reviewer. Review the cumulative code delivered since the last code-review checkpoint. Focus on blockers, correctness bugs, security/reliability problems, regressions, and critical maintainability issues. Avoid blocking on style or subjective polish.
|
|
217
|
+
|
|
218
|
+
Tasks in/around this review batch: {json.dumps(batch_task_ids)}
|
|
219
|
+
Review base commit: {base_commit}
|
|
220
|
+
Current diff summary:
|
|
221
|
+
{diff_stat}
|
|
222
|
+
|
|
223
|
+
Inspect the repository and diff directly. Do not edit code.
|
|
224
|
+
|
|
225
|
+
Return ONLY JSON:
|
|
226
|
+
{{
|
|
227
|
+
"verdict":"PASS"|"CHANGES_REQUIRED",
|
|
228
|
+
"findings":[{{"severity":"BLOCKER"|"BUG"|"CRITICAL"|"NON_BLOCKING", "title":"...", "evidence":"file/behavior-specific evidence", "affected_task_ids":["TASK-ID"]}}],
|
|
229
|
+
"summary":"concise review conclusion"
|
|
230
|
+
}}
|
|
231
|
+
CHANGES_REQUIRED only when at least one finding is BLOCKER, BUG, or CRITICAL.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def review_fix(review_json: str) -> str:
|
|
236
|
+
return f"""You are an implementer assigned to resolve critical findings from an independent cumulative code review on the integration branch. Work in the isolated review-fix worktree.
|
|
237
|
+
|
|
238
|
+
Review findings:
|
|
239
|
+
---
|
|
240
|
+
{review_json}
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
Use engineering judgment to fix the root causes without broad unrelated refactors. Do not edit BACKLOG.md/.manager, push, create PRs, publish, deploy, or commit. Run relevant validation and summarize the changes.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def review_qa(review_json: str, verification_commands: list[str]) -> str:
|
|
248
|
+
return f"""You are independent QA verifying fixes for cumulative code-review findings.
|
|
249
|
+
|
|
250
|
+
Original code review:
|
|
251
|
+
---
|
|
252
|
+
{review_json}
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
Configured verification commands: {json.dumps(verification_commands)}
|
|
256
|
+
|
|
257
|
+
Inspect and test the disposable worktree. Do not edit source code. Return ONLY JSON:
|
|
258
|
+
{{"verdict":"PASS"|"FAIL"|"UNVERIFIED", "summary":"...", "failures":["concrete evidence"], "commands_run":[{{"command":"...", "result":"..."}}]}}
|
|
259
|
+
"""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .claude import ClaudeClient
|
|
4
|
+
from .codex import CodexClient
|
|
5
|
+
from .config import Config
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_client(config: Config, hooks_dir: Path) -> ClaudeClient | CodexClient:
|
|
9
|
+
if config.provider == "codex":
|
|
10
|
+
return CodexClient(config, hooks_dir)
|
|
11
|
+
return ClaudeClient(config, hooks_dir)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Offline run reports: no server, external assets, or model calls."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .state import load_state
|
|
9
|
+
from .usage import summarize
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def task_evidence(run_dir: Path, task) -> dict:
|
|
13
|
+
if not task.last_qa_path:
|
|
14
|
+
return {"verification_level": "not_verified"}
|
|
15
|
+
path = Path(task.last_qa_path).resolve()
|
|
16
|
+
if not path.is_relative_to(run_dir.resolve()) or not path.exists():
|
|
17
|
+
return {"verification_level": "evidence_unavailable"}
|
|
18
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def review_text(report: dict, task_id: str | None = None) -> str:
|
|
22
|
+
lines = [f'Task Harbor review: {report["run_id"]}', f'Branch: {report["integration_branch"]}']
|
|
23
|
+
selected = [t for t in report["tasks"] if task_id is None or t["id"] == task_id]
|
|
24
|
+
if not selected:
|
|
25
|
+
raise ValueError(f"Task not found: {task_id}")
|
|
26
|
+
for task in selected:
|
|
27
|
+
evidence = task["evidence"]
|
|
28
|
+
lines += ["", f'{task["id"]}: {task["title"]}', f'Status: {task["status"]}',
|
|
29
|
+
f'Checkpoint: {evidence.get("checkpoint_commit", "not recorded")}',
|
|
30
|
+
f'Verification: {evidence.get("verification_level", "legacy/unknown")}',
|
|
31
|
+
"Task agreement: " + json.dumps(task["contract"], indent=2),
|
|
32
|
+
"Acceptance: " + json.dumps(evidence.get("acceptance", []), indent=2),
|
|
33
|
+
"Executed checks: " + json.dumps(evidence.get("harness_checks", []), indent=2),
|
|
34
|
+
"Before/after reproduction: " + json.dumps(evidence.get("reproduction", {}), indent=2),
|
|
35
|
+
"Artifacts: " + json.dumps(evidence.get("artifacts", []), indent=2),
|
|
36
|
+
"Failure classification: " + json.dumps(evidence.get("failure_classification", []), indent=2),
|
|
37
|
+
"Human feedback: " + json.dumps([f for f in report.get("feedback", []) if f.get("task") == task["id"]], indent=2),
|
|
38
|
+
"Unverified / risks: " + json.dumps(evidence.get("risks", []))]
|
|
39
|
+
for file in evidence.get("changes", {}).get("files", []):
|
|
40
|
+
lines += [f'\nFILE: {file["path"]}', "Review flags: " + ", ".join(file["warnings"]), file["diff"]]
|
|
41
|
+
return "\n".join(lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_report(run_dir: Path) -> dict:
|
|
45
|
+
state = load_state(run_dir / "state.json")
|
|
46
|
+
usage_file = run_dir / "usage.json"
|
|
47
|
+
usage = json.loads(usage_file.read_text(encoding="utf-8")) if usage_file.exists() else {"calls": []}
|
|
48
|
+
feedback_path = run_dir / "feedback.json"
|
|
49
|
+
feedback = json.loads(feedback_path.read_text(encoding="utf-8")) if feedback_path.exists() else []
|
|
50
|
+
completed = sum(t.status.value == "COMPLETE" for t in state.tasks.values())
|
|
51
|
+
totals = summarize(usage)
|
|
52
|
+
return {"run_id": state.run_id, "provider": state.provider, "integration_branch": state.integration_branch,
|
|
53
|
+
"feedback": feedback, "active_review_minutes": sum(f["active_review_minutes"] for f in feedback),
|
|
54
|
+
"paused": state.paused, "stop_reason": state.stop_reason,
|
|
55
|
+
"completed": completed, "total": len(state.tasks), "usage": totals,
|
|
56
|
+
"reported_cost_per_completed_task": totals["reported_cost_usd"] / completed if completed else None,
|
|
57
|
+
"tasks": [{"id": t.task_id, "title": t.title, "status": t.status.value,
|
|
58
|
+
"dependencies": t.dependencies, "blocked_reason": t.blocked_reason,
|
|
59
|
+
"next_action": t.next_action, "generation": t.generation,
|
|
60
|
+
"checkpoint": t.checkpoint_commit,
|
|
61
|
+
"contract": t.metadata.get("contract", {}),
|
|
62
|
+
"evidence": task_evidence(run_dir, t),
|
|
63
|
+
"usage": summarize({"calls": [c for c in usage["calls"] if c.get("task") == t.task_id]})}
|
|
64
|
+
for t in state.tasks.values()]}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def write_html(report: dict, output: Path) -> None:
|
|
68
|
+
esc = lambda value: html.escape(str(value if value is not None else "—"))
|
|
69
|
+
rows = "".join("<tr>" + "".join(f"<td>{esc(t[k])}</td>" for k in
|
|
70
|
+
("id", "title", "status", "blocked_reason", "next_action")) + "</tr>" for t in report["tasks"])
|
|
71
|
+
details = "".join(f'<details><summary>{esc(t["id"])}: {esc(t["title"])}</summary><pre>{esc(review_text(report, t["id"]))}</pre></details>' for t in report["tasks"])
|
|
72
|
+
output.write_text(f'''<!doctype html><html lang="en"><meta charset="utf-8">
|
|
73
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"><title>Task Harbor · Run report</title>
|
|
74
|
+
<style>body{{font:16px system-ui;margin:3rem;background:#101923;color:#e2eaf2}}h1{{color:#68debf}}
|
|
75
|
+
table{{border-collapse:collapse;width:100%}}td,th{{padding:1rem;text-align:left;border-bottom:1px solid #35414d}}
|
|
76
|
+
pre{{white-space:pre-wrap}}.scroll{{overflow:auto}}p{{line-height:1.7}}</style>
|
|
77
|
+
<h1>Task Harbor</h1><p>Run {esc(report["run_id"])} · {report["completed"]}/{report["total"]} complete</p>
|
|
78
|
+
<p>{esc(report["integration_branch"])}<br>{esc(report["stop_reason"])}</p>
|
|
79
|
+
<h2>Usage</h2><pre>{esc(json.dumps(report["usage"], indent=2))}</pre>
|
|
80
|
+
<p>Costs are CLI estimates. Calls without reported cost are unknown, not free. Snapshot; regenerate to refresh.</p>
|
|
81
|
+
<div class="scroll"><table><thead><tr><th>Task</th><th>Title</th><th>Status</th><th>Blocked</th><th>Next action</th></tr></thead>
|
|
82
|
+
<tbody>{rows}</tbody></table></div><h2>Evidence and diffs</h2>{details}</html>''', encoding="utf-8")
|
opus_manager/state.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .models import ManagerState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def atomic_write_json(path: Path, payload: dict) -> None:
|
|
12
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
13
|
+
fd, tmp = tempfile.mkstemp(prefix=path.name, suffix=".tmp", dir=path.parent)
|
|
14
|
+
try:
|
|
15
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
16
|
+
json.dump(payload, f, indent=2)
|
|
17
|
+
f.write("\n")
|
|
18
|
+
f.flush()
|
|
19
|
+
os.fsync(f.fileno())
|
|
20
|
+
os.replace(tmp, path)
|
|
21
|
+
finally:
|
|
22
|
+
if os.path.exists(tmp):
|
|
23
|
+
os.unlink(tmp)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_state(path: Path, state: ManagerState) -> None:
|
|
27
|
+
atomic_write_json(path, state.to_dict())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_state(path: Path) -> ManagerState:
|
|
31
|
+
return ManagerState.from_dict(json.loads(path.read_text(encoding="utf-8")))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def append_event(path: Path, event: dict) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
with path.open("a", encoding="utf-8") as f:
|
|
37
|
+
f.write(json.dumps(event, sort_keys=True) + "\n")
|