taskledger 0.1.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.
- taskledger/__init__.py +5 -0
- taskledger/__main__.py +6 -0
- taskledger/_version.py +24 -0
- taskledger/api/__init__.py +13 -0
- taskledger/api/handoff.py +247 -0
- taskledger/api/introductions.py +9 -0
- taskledger/api/locks.py +4 -0
- taskledger/api/plans.py +31 -0
- taskledger/api/project.py +185 -0
- taskledger/api/questions.py +19 -0
- taskledger/api/search.py +87 -0
- taskledger/api/task_runs.py +38 -0
- taskledger/api/tasks.py +61 -0
- taskledger/cli.py +600 -0
- taskledger/cli_actor.py +196 -0
- taskledger/cli_common.py +617 -0
- taskledger/cli_implement.py +409 -0
- taskledger/cli_migrate.py +328 -0
- taskledger/cli_misc.py +984 -0
- taskledger/cli_plan.py +478 -0
- taskledger/cli_question.py +350 -0
- taskledger/cli_task.py +257 -0
- taskledger/cli_validate.py +285 -0
- taskledger/command_inventory.py +125 -0
- taskledger/domain/__init__.py +2 -0
- taskledger/domain/models.py +1697 -0
- taskledger/domain/policies.py +542 -0
- taskledger/domain/states.py +320 -0
- taskledger/errors.py +165 -0
- taskledger/exchange.py +343 -0
- taskledger/ids.py +19 -0
- taskledger/py.typed +0 -0
- taskledger/search.py +349 -0
- taskledger/services/__init__.py +1 -0
- taskledger/services/actors.py +245 -0
- taskledger/services/dashboard.py +306 -0
- taskledger/services/doctor.py +435 -0
- taskledger/services/handoff.py +1029 -0
- taskledger/services/handoff_lifecycle.py +154 -0
- taskledger/services/navigation.py +930 -0
- taskledger/services/phase5_lock_transfer.py +96 -0
- taskledger/services/plan_lint.py +397 -0
- taskledger/services/serve_read_model.py +852 -0
- taskledger/services/tasks.py +4224 -0
- taskledger/services/validation.py +221 -0
- taskledger/services/web_dashboard.py +1742 -0
- taskledger/storage/__init__.py +39 -0
- taskledger/storage/atomic.py +57 -0
- taskledger/storage/common.py +90 -0
- taskledger/storage/events.py +98 -0
- taskledger/storage/frontmatter.py +57 -0
- taskledger/storage/indexes.py +42 -0
- taskledger/storage/init.py +187 -0
- taskledger/storage/locks.py +83 -0
- taskledger/storage/meta.py +103 -0
- taskledger/storage/migrations.py +207 -0
- taskledger/storage/paths.py +166 -0
- taskledger/storage/project_config.py +393 -0
- taskledger/storage/repos.py +256 -0
- taskledger/storage/task_store.py +836 -0
- taskledger/timeutils.py +7 -0
- taskledger-0.1.0.dist-info/METADATA +411 -0
- taskledger-0.1.0.dist-info/RECORD +67 -0
- taskledger-0.1.0.dist-info/WHEEL +5 -0
- taskledger-0.1.0.dist-info/entry_points.txt +2 -0
- taskledger-0.1.0.dist-info/licenses/LICENSE +201 -0
- taskledger-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
from taskledger.domain.models import CodeChangeRecord, TaskRunRecord, TaskTodo
|
|
8
|
+
from taskledger.domain.policies import derive_active_stage
|
|
9
|
+
from taskledger.domain.states import (
|
|
10
|
+
ContextFor,
|
|
11
|
+
ContextScope,
|
|
12
|
+
HandoffMode,
|
|
13
|
+
normalize_context_for,
|
|
14
|
+
normalize_context_format,
|
|
15
|
+
normalize_context_scope,
|
|
16
|
+
normalize_handoff_mode,
|
|
17
|
+
)
|
|
18
|
+
from taskledger.errors import LaunchError
|
|
19
|
+
from taskledger.storage.locks import lock_is_expired, lock_status, read_lock
|
|
20
|
+
from taskledger.storage.task_store import (
|
|
21
|
+
list_changes,
|
|
22
|
+
list_plans,
|
|
23
|
+
list_questions,
|
|
24
|
+
list_runs,
|
|
25
|
+
load_links,
|
|
26
|
+
load_requirements,
|
|
27
|
+
load_todos,
|
|
28
|
+
resolve_introduction,
|
|
29
|
+
resolve_plan,
|
|
30
|
+
resolve_run,
|
|
31
|
+
resolve_task,
|
|
32
|
+
resolve_v2_paths,
|
|
33
|
+
task_lock_path,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ContextRequest:
|
|
39
|
+
mode: HandoffMode
|
|
40
|
+
context_for: ContextFor
|
|
41
|
+
scope: ContextScope = "task"
|
|
42
|
+
todo_id: str | None = None
|
|
43
|
+
focus_run_id: str | None = None
|
|
44
|
+
format_name: str = "markdown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def render_handoff(
|
|
48
|
+
workspace_root: Path,
|
|
49
|
+
task_ref: str,
|
|
50
|
+
*,
|
|
51
|
+
mode: str | None = None,
|
|
52
|
+
context_for: str | None = None,
|
|
53
|
+
scope: str | None = None,
|
|
54
|
+
todo_id: str | None = None,
|
|
55
|
+
focus_run_id: str | None = None,
|
|
56
|
+
format_name: str = "markdown",
|
|
57
|
+
) -> str | dict[str, object]:
|
|
58
|
+
request = build_context_request(
|
|
59
|
+
mode=mode,
|
|
60
|
+
context_for=context_for,
|
|
61
|
+
scope=scope,
|
|
62
|
+
todo_id=todo_id,
|
|
63
|
+
focus_run_id=focus_run_id,
|
|
64
|
+
format_name=format_name,
|
|
65
|
+
)
|
|
66
|
+
payload = build_handoff_payload(
|
|
67
|
+
workspace_root,
|
|
68
|
+
task_ref,
|
|
69
|
+
mode=request.mode,
|
|
70
|
+
context_for=request.context_for,
|
|
71
|
+
scope=request.scope,
|
|
72
|
+
todo_id=request.todo_id,
|
|
73
|
+
focus_run_id=request.focus_run_id,
|
|
74
|
+
format_name=request.format_name,
|
|
75
|
+
)
|
|
76
|
+
if request.format_name == "json":
|
|
77
|
+
return payload
|
|
78
|
+
return render_markdown_handoff(payload)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_context_request(
|
|
82
|
+
*,
|
|
83
|
+
mode: str | None = None,
|
|
84
|
+
context_for: str | None = None,
|
|
85
|
+
scope: str | None = None,
|
|
86
|
+
todo_id: str | None = None,
|
|
87
|
+
focus_run_id: str | None = None,
|
|
88
|
+
format_name: str = "markdown",
|
|
89
|
+
) -> ContextRequest:
|
|
90
|
+
resolved_format = normalize_context_format(format_name)
|
|
91
|
+
resolved_mode = normalize_handoff_mode(_canonical_mode(mode)) if mode else None
|
|
92
|
+
resolved_for = normalize_context_for(context_for) if context_for else None
|
|
93
|
+
|
|
94
|
+
if resolved_for is None and resolved_mode is None:
|
|
95
|
+
resolved_for = "full"
|
|
96
|
+
resolved_mode = "full"
|
|
97
|
+
elif resolved_for is None:
|
|
98
|
+
assert resolved_mode is not None
|
|
99
|
+
resolved_for = _default_context_for(resolved_mode)
|
|
100
|
+
else:
|
|
101
|
+
inferred_mode = _mode_for_context_for(resolved_for)
|
|
102
|
+
if resolved_mode is None:
|
|
103
|
+
resolved_mode = inferred_mode
|
|
104
|
+
elif resolved_mode != inferred_mode:
|
|
105
|
+
raise LaunchError(
|
|
106
|
+
"Context role "
|
|
107
|
+
f"{resolved_for!r} is incompatible with mode {resolved_mode!r}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
assert resolved_mode is not None
|
|
111
|
+
explicit_scope = normalize_context_scope(scope) if scope else None
|
|
112
|
+
resolved_scope = explicit_scope or "task"
|
|
113
|
+
if todo_id is not None:
|
|
114
|
+
if explicit_scope is not None and explicit_scope != "todo":
|
|
115
|
+
raise LaunchError("--todo implies --scope todo")
|
|
116
|
+
resolved_scope = "todo"
|
|
117
|
+
if focus_run_id is not None:
|
|
118
|
+
if explicit_scope is not None and explicit_scope != "run":
|
|
119
|
+
raise LaunchError("--run implies --scope run")
|
|
120
|
+
resolved_scope = "run"
|
|
121
|
+
if resolved_scope == "todo" and not todo_id:
|
|
122
|
+
raise LaunchError("--scope todo requires --todo")
|
|
123
|
+
if resolved_scope == "run" and not focus_run_id:
|
|
124
|
+
raise LaunchError("--scope run requires --run")
|
|
125
|
+
if (
|
|
126
|
+
resolved_for in {"spec-reviewer", "code-reviewer"}
|
|
127
|
+
and focus_run_id is None
|
|
128
|
+
and explicit_scope is None
|
|
129
|
+
):
|
|
130
|
+
raise LaunchError(f"{resolved_for} context requires --run or --scope task")
|
|
131
|
+
|
|
132
|
+
return ContextRequest(
|
|
133
|
+
mode=resolved_mode,
|
|
134
|
+
context_for=resolved_for,
|
|
135
|
+
scope=resolved_scope,
|
|
136
|
+
todo_id=todo_id,
|
|
137
|
+
focus_run_id=focus_run_id,
|
|
138
|
+
format_name=resolved_format,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_handoff_payload(
|
|
143
|
+
workspace_root: Path,
|
|
144
|
+
task_ref: str,
|
|
145
|
+
*,
|
|
146
|
+
mode: str | None = None,
|
|
147
|
+
context_for: str | None = None,
|
|
148
|
+
scope: str | None = None,
|
|
149
|
+
todo_id: str | None = None,
|
|
150
|
+
focus_run_id: str | None = None,
|
|
151
|
+
format_name: str = "markdown",
|
|
152
|
+
) -> dict[str, object]:
|
|
153
|
+
request = build_context_request(
|
|
154
|
+
mode=mode,
|
|
155
|
+
context_for=context_for,
|
|
156
|
+
scope=scope,
|
|
157
|
+
todo_id=todo_id,
|
|
158
|
+
focus_run_id=focus_run_id,
|
|
159
|
+
format_name=format_name,
|
|
160
|
+
)
|
|
161
|
+
task = resolve_task(workspace_root, task_ref)
|
|
162
|
+
intro = (
|
|
163
|
+
resolve_introduction(workspace_root, task.introduction_ref)
|
|
164
|
+
if task.introduction_ref
|
|
165
|
+
else None
|
|
166
|
+
)
|
|
167
|
+
plans = list_plans(workspace_root, task.id)
|
|
168
|
+
questions = list_questions(workspace_root, task.id)
|
|
169
|
+
runs = list_runs(workspace_root, task.id)
|
|
170
|
+
todos = list(load_todos(workspace_root, task.id).todos)
|
|
171
|
+
changes = list_changes(workspace_root, task.id)
|
|
172
|
+
accepted_plan = (
|
|
173
|
+
resolve_plan(workspace_root, task.id, version=task.accepted_plan_version)
|
|
174
|
+
if task.accepted_plan_version is not None
|
|
175
|
+
else None
|
|
176
|
+
)
|
|
177
|
+
latest_impl = _latest_run(runs, "implementation")
|
|
178
|
+
latest_validation = _latest_run(runs, "validation")
|
|
179
|
+
lock = read_lock(task_lock_path(resolve_v2_paths(workspace_root), task.id))
|
|
180
|
+
active_stage = (
|
|
181
|
+
None
|
|
182
|
+
if lock is None or lock_is_expired(lock)
|
|
183
|
+
else derive_active_stage(lock, runs)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
dependencies = []
|
|
187
|
+
for requirement in (
|
|
188
|
+
item.task_id for item in load_requirements(workspace_root, task.id).requirements
|
|
189
|
+
):
|
|
190
|
+
dependency = resolve_task(workspace_root, requirement)
|
|
191
|
+
dependencies.append(
|
|
192
|
+
{
|
|
193
|
+
"task_id": dependency.id,
|
|
194
|
+
"title": dependency.title,
|
|
195
|
+
"status_stage": dependency.status_stage,
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
focus = _resolve_focus(workspace_root, task.id, request, todos, runs, changes)
|
|
200
|
+
open_questions = [item.to_dict() for item in questions if item.status == "open"]
|
|
201
|
+
answered_questions = [
|
|
202
|
+
item.to_dict() for item in questions if item.status == "answered"
|
|
203
|
+
]
|
|
204
|
+
dismissed_questions = [
|
|
205
|
+
item.to_dict() for item in questions if item.status == "dismissed"
|
|
206
|
+
]
|
|
207
|
+
validation_history = [
|
|
208
|
+
run.to_dict()
|
|
209
|
+
for run in runs
|
|
210
|
+
if run.run_type == "validation" and run.status != "running"
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
validation_status_report = None
|
|
214
|
+
if request.context_for in {"validator", "full"}:
|
|
215
|
+
from taskledger.services.validation import build_validation_gate_report
|
|
216
|
+
|
|
217
|
+
validation_status_report = build_validation_gate_report(workspace_root, task)
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
"kind": "task_handoff",
|
|
221
|
+
"mode": request.mode,
|
|
222
|
+
"context_for": request.context_for,
|
|
223
|
+
"scope": request.scope,
|
|
224
|
+
"context_format": request.format_name,
|
|
225
|
+
"focus": focus,
|
|
226
|
+
"task": {**task.to_dict(), "active_stage": active_stage},
|
|
227
|
+
"introduction": intro.to_dict() if intro is not None else None,
|
|
228
|
+
"guardrails": _guardrails_for_context_for(request.context_for),
|
|
229
|
+
"accepted_plan": accepted_plan.to_dict() if accepted_plan is not None else None,
|
|
230
|
+
"plans": [plan.to_dict() for plan in plans],
|
|
231
|
+
"questions": {
|
|
232
|
+
"open": open_questions,
|
|
233
|
+
"answered": answered_questions,
|
|
234
|
+
"dismissed": dismissed_questions,
|
|
235
|
+
},
|
|
236
|
+
"todos": [todo.to_dict() for todo in todos],
|
|
237
|
+
"todo_summary": _todo_summary(todos, request.todo_id),
|
|
238
|
+
"file_links": [
|
|
239
|
+
item.to_dict() for item in load_links(workspace_root, task.id).links
|
|
240
|
+
],
|
|
241
|
+
"dependencies": dependencies,
|
|
242
|
+
"runs": {
|
|
243
|
+
"latest_planning": _run_to_dict(_latest_run(runs, "planning")),
|
|
244
|
+
"latest_implementation": _run_to_dict(latest_impl),
|
|
245
|
+
"latest_validation": _run_to_dict(latest_validation),
|
|
246
|
+
},
|
|
247
|
+
"lock": lock.to_dict() if lock is not None else None,
|
|
248
|
+
"lock_status": lock_status(lock),
|
|
249
|
+
"changes": [change.to_dict() for change in changes],
|
|
250
|
+
"focused_changes": focus["focused_changes"],
|
|
251
|
+
"validation_history": validation_history,
|
|
252
|
+
"validation_status": validation_status_report,
|
|
253
|
+
"review_contract": (
|
|
254
|
+
{
|
|
255
|
+
"role": request.context_for,
|
|
256
|
+
"scope": request.scope,
|
|
257
|
+
"guardrails": _guardrails_for_context_for(request.context_for),
|
|
258
|
+
}
|
|
259
|
+
if request.context_for in {"reviewer", "spec-reviewer", "code-reviewer"}
|
|
260
|
+
else None
|
|
261
|
+
),
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def render_markdown_handoff(payload: dict[str, object]) -> str:
|
|
266
|
+
mode = str(payload["mode"])
|
|
267
|
+
context_for = str(payload.get("context_for") or mode)
|
|
268
|
+
task = payload["task"]
|
|
269
|
+
assert isinstance(task, dict)
|
|
270
|
+
title_prefix = {
|
|
271
|
+
"planning": "Planning Context",
|
|
272
|
+
"implementation": "Implementation Context",
|
|
273
|
+
"validation": "Validation Context",
|
|
274
|
+
"review": "Review Context",
|
|
275
|
+
"full": "Task Dossier",
|
|
276
|
+
}.get(mode, "Task Context")
|
|
277
|
+
lines = [f"# {title_prefix}: {task['title']}", ""]
|
|
278
|
+
_append_worker_role(lines, payload)
|
|
279
|
+
_append_worker_contract(lines, payload)
|
|
280
|
+
_append_task_section(lines, task)
|
|
281
|
+
_append_description(lines, task)
|
|
282
|
+
_append_intro(lines, payload.get("introduction"))
|
|
283
|
+
_append_dependencies(lines, payload["dependencies"])
|
|
284
|
+
_append_file_links(lines, payload["file_links"])
|
|
285
|
+
_append_plans(lines, payload["plans"])
|
|
286
|
+
_append_questions(lines, payload["questions"])
|
|
287
|
+
if context_for in {"planner"}:
|
|
288
|
+
_append_guardrails(lines, payload["guardrails"])
|
|
289
|
+
_append_required_commands(lines, payload.get("accepted_plan"))
|
|
290
|
+
_append_required_output(lines, context_for)
|
|
291
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
292
|
+
if context_for in {
|
|
293
|
+
"implementer",
|
|
294
|
+
"validator",
|
|
295
|
+
"reviewer",
|
|
296
|
+
"spec-reviewer",
|
|
297
|
+
"code-reviewer",
|
|
298
|
+
"full",
|
|
299
|
+
}:
|
|
300
|
+
_append_accepted_plan(lines, payload.get("accepted_plan"))
|
|
301
|
+
if context_for == "implementer":
|
|
302
|
+
_append_acceptance_criteria(lines, payload.get("accepted_plan"))
|
|
303
|
+
if payload.get("scope") == "todo":
|
|
304
|
+
_append_focused_todo(lines, payload.get("focus"))
|
|
305
|
+
_append_other_todo_summary(lines, payload.get("todo_summary"))
|
|
306
|
+
else:
|
|
307
|
+
_append_todos(lines, payload["todos"])
|
|
308
|
+
_append_lock_and_runs(lines, payload)
|
|
309
|
+
_append_required_commands(lines, payload.get("accepted_plan"))
|
|
310
|
+
_append_required_output(lines, context_for)
|
|
311
|
+
elif context_for == "validator":
|
|
312
|
+
_append_todo_completion_summary(lines, payload.get("todo_summary"))
|
|
313
|
+
_append_implementation_summary(lines, payload["runs"])
|
|
314
|
+
_append_change_log(lines, payload["changes"])
|
|
315
|
+
_append_validation_status(lines, payload.get("validation_status"))
|
|
316
|
+
_append_validation_history(lines, payload["validation_history"])
|
|
317
|
+
_append_required_commands(lines, payload.get("accepted_plan"))
|
|
318
|
+
_append_required_output(lines, context_for)
|
|
319
|
+
elif context_for == "spec-reviewer":
|
|
320
|
+
_append_acceptance_criteria(lines, payload.get("accepted_plan"))
|
|
321
|
+
_append_focused_run(lines, payload.get("focus"))
|
|
322
|
+
_append_focused_changes(lines, payload.get("focused_changes"))
|
|
323
|
+
_append_plan_deviations(lines, payload.get("focus"))
|
|
324
|
+
_append_todo_updates(lines, payload.get("focus"))
|
|
325
|
+
_append_spec_review(lines)
|
|
326
|
+
_append_required_output(lines, context_for)
|
|
327
|
+
elif context_for == "code-reviewer":
|
|
328
|
+
_append_focused_run(lines, payload.get("focus"))
|
|
329
|
+
_append_focused_changes(lines, payload.get("focused_changes"))
|
|
330
|
+
_append_commands_already_run(lines, payload.get("focus"))
|
|
331
|
+
_append_code_quality_review(lines)
|
|
332
|
+
_append_required_output(lines, context_for)
|
|
333
|
+
elif context_for == "reviewer":
|
|
334
|
+
_append_focused_run(lines, payload.get("focus"))
|
|
335
|
+
_append_focused_changes(lines, payload.get("focused_changes"))
|
|
336
|
+
_append_required_output(lines, context_for)
|
|
337
|
+
elif context_for == "full":
|
|
338
|
+
_append_acceptance_criteria(lines, payload.get("accepted_plan"))
|
|
339
|
+
_append_todos(lines, payload["todos"])
|
|
340
|
+
_append_lock_and_runs(lines, payload)
|
|
341
|
+
_append_implementation_summary(lines, payload["runs"])
|
|
342
|
+
_append_change_log(lines, payload["changes"])
|
|
343
|
+
_append_validation_status(lines, payload.get("validation_status"))
|
|
344
|
+
_append_validation_history(lines, payload["validation_history"])
|
|
345
|
+
_append_required_commands(lines, payload.get("accepted_plan"))
|
|
346
|
+
_append_required_output(lines, context_for)
|
|
347
|
+
else:
|
|
348
|
+
_append_guardrails(lines, payload["guardrails"])
|
|
349
|
+
_append_required_output(lines, context_for)
|
|
350
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _append_worker_role(lines: list[str], payload: dict[str, object]) -> None:
|
|
354
|
+
focus = payload.get("focus")
|
|
355
|
+
focused_todo = "none"
|
|
356
|
+
focused_run = "none"
|
|
357
|
+
if isinstance(focus, dict):
|
|
358
|
+
focused_todo = str(focus.get("todo_id") or "none")
|
|
359
|
+
focused_run = str(focus.get("focus_run_id") or "none")
|
|
360
|
+
lines.extend(
|
|
361
|
+
[
|
|
362
|
+
"## Worker Role",
|
|
363
|
+
"",
|
|
364
|
+
f"- role: {payload.get('context_for')}",
|
|
365
|
+
f"- lifecycle_mode: {payload.get('mode')}",
|
|
366
|
+
f"- scope: {payload.get('scope')}",
|
|
367
|
+
f"- focused_todo: {focused_todo}",
|
|
368
|
+
f"- focused_run: {focused_run}",
|
|
369
|
+
"",
|
|
370
|
+
]
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _append_worker_contract(lines: list[str], payload: dict[str, object]) -> None:
|
|
375
|
+
context_for = str(payload.get("context_for") or "full")
|
|
376
|
+
must_items, must_not_items = _worker_contract(context_for)
|
|
377
|
+
lines.extend(["## Worker Contract", "", "You must:"])
|
|
378
|
+
for item in must_items:
|
|
379
|
+
lines.append(f"- {item}")
|
|
380
|
+
lines.extend(["", "You must not:"])
|
|
381
|
+
for item in must_not_items:
|
|
382
|
+
lines.append(f"- {item}")
|
|
383
|
+
lines.append("")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _append_task_section(lines: list[str], task: dict[str, object]) -> None:
|
|
387
|
+
lines.extend(
|
|
388
|
+
[
|
|
389
|
+
"## Task",
|
|
390
|
+
"",
|
|
391
|
+
f"- id: {task['id']}",
|
|
392
|
+
f"- slug: {task['slug']}",
|
|
393
|
+
f"- status_stage: {task['status_stage']}",
|
|
394
|
+
f"- active_stage: {task.get('active_stage') or 'none'}",
|
|
395
|
+
f"- priority: {task.get('priority') or 'unset'}",
|
|
396
|
+
"- labels: "
|
|
397
|
+
+ (", ".join(cast(list[str], task.get("labels") or [])) or "none"),
|
|
398
|
+
f"- owner: {task.get('owner') or 'unassigned'}",
|
|
399
|
+
"",
|
|
400
|
+
]
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _append_description(lines: list[str], task: dict[str, object]) -> None:
|
|
405
|
+
lines.extend(["## Description", "", str(task.get("body") or ""), ""])
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _append_intro(lines: list[str], intro: object) -> None:
|
|
409
|
+
if not isinstance(intro, dict):
|
|
410
|
+
return
|
|
411
|
+
lines.extend(["## Introduction", "", str(intro.get("body") or ""), ""])
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _append_dependencies(lines: list[str], dependencies: object) -> None:
|
|
415
|
+
if not isinstance(dependencies, list):
|
|
416
|
+
return
|
|
417
|
+
lines.extend(["## Requirements", ""])
|
|
418
|
+
for item in dependencies:
|
|
419
|
+
if isinstance(item, dict):
|
|
420
|
+
lines.append(
|
|
421
|
+
f"- {item['task_id']}: {item['title']} — {item['status_stage']}"
|
|
422
|
+
)
|
|
423
|
+
if not dependencies:
|
|
424
|
+
lines.append("- none")
|
|
425
|
+
lines.append("")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _append_file_links(lines: list[str], file_links: object) -> None:
|
|
429
|
+
if not isinstance(file_links, list):
|
|
430
|
+
return
|
|
431
|
+
lines.extend(["## Linked Files", ""])
|
|
432
|
+
for item in file_links:
|
|
433
|
+
if isinstance(item, dict):
|
|
434
|
+
lines.append(f"- @{item['path']} [{item['kind']}]")
|
|
435
|
+
if not file_links:
|
|
436
|
+
lines.append("- none")
|
|
437
|
+
lines.append("")
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _append_plans(lines: list[str], plans: object) -> None:
|
|
441
|
+
if not isinstance(plans, list):
|
|
442
|
+
return
|
|
443
|
+
lines.extend(["## Existing Plans", ""])
|
|
444
|
+
for item in plans:
|
|
445
|
+
if isinstance(item, dict):
|
|
446
|
+
lines.append(f"- v{item['plan_version']} {item['status']}")
|
|
447
|
+
if not plans:
|
|
448
|
+
lines.append("- none")
|
|
449
|
+
lines.append("")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _append_questions(lines: list[str], payload: object) -> None:
|
|
453
|
+
if not isinstance(payload, dict):
|
|
454
|
+
return
|
|
455
|
+
lines.extend(["## Questions", "", "### Open", ""])
|
|
456
|
+
open_items = payload.get("open")
|
|
457
|
+
if isinstance(open_items, list) and open_items:
|
|
458
|
+
for item in open_items:
|
|
459
|
+
if isinstance(item, dict):
|
|
460
|
+
lines.append(f"- {item['id']}: {item['question']}")
|
|
461
|
+
else:
|
|
462
|
+
lines.append("- none")
|
|
463
|
+
lines.extend(["", "### Answered", ""])
|
|
464
|
+
answered_items = payload.get("answered")
|
|
465
|
+
if isinstance(answered_items, list) and answered_items:
|
|
466
|
+
for item in answered_items:
|
|
467
|
+
if isinstance(item, dict):
|
|
468
|
+
lines.append(
|
|
469
|
+
f"- {item['question']} -> {item.get('answer') or '(none)'}"
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
lines.append("- none")
|
|
473
|
+
lines.append("")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _append_guardrails(lines: list[str], guardrails: object) -> None:
|
|
477
|
+
if not isinstance(guardrails, list):
|
|
478
|
+
return
|
|
479
|
+
lines.extend(["## Guardrails", ""])
|
|
480
|
+
for item in guardrails:
|
|
481
|
+
lines.append(f"- {item}")
|
|
482
|
+
lines.append("")
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _append_accepted_plan(lines: list[str], accepted_plan: object) -> None:
|
|
486
|
+
if not isinstance(accepted_plan, dict):
|
|
487
|
+
return
|
|
488
|
+
lines.extend(["## Accepted Plan", "", str(accepted_plan.get("body") or ""), ""])
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _append_acceptance_criteria(lines: list[str], accepted_plan: object) -> None:
|
|
492
|
+
if not isinstance(accepted_plan, dict):
|
|
493
|
+
return
|
|
494
|
+
criteria = accepted_plan.get("criteria")
|
|
495
|
+
lines.extend(["## Acceptance Criteria", ""])
|
|
496
|
+
if isinstance(criteria, list) and criteria:
|
|
497
|
+
for item in criteria:
|
|
498
|
+
if isinstance(item, dict):
|
|
499
|
+
lines.append(f"- {item['id']}: {item['text']}")
|
|
500
|
+
else:
|
|
501
|
+
lines.append("- none")
|
|
502
|
+
lines.append("")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _append_todos(lines: list[str], todos: object) -> None:
|
|
506
|
+
if not isinstance(todos, list):
|
|
507
|
+
return
|
|
508
|
+
done_count = sum(1 for item in todos if isinstance(item, dict) and item.get("done"))
|
|
509
|
+
total_count = len(todos)
|
|
510
|
+
lines.extend(["## Todo Checklist", ""])
|
|
511
|
+
if total_count == 0:
|
|
512
|
+
lines.append("- none (no todos)")
|
|
513
|
+
else:
|
|
514
|
+
lines.append(f"Progress: {done_count}/{total_count} done")
|
|
515
|
+
lines.append("")
|
|
516
|
+
for item in todos:
|
|
517
|
+
if isinstance(item, dict):
|
|
518
|
+
mark = "x" if item.get("done") else " "
|
|
519
|
+
lines.append(f"- [{mark}] {item['id']}: {item['text']}")
|
|
520
|
+
lines.append("")
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _append_focused_todo(lines: list[str], focus: object) -> None:
|
|
524
|
+
lines.extend(["## Focused Todo", ""])
|
|
525
|
+
if not isinstance(focus, dict) or not isinstance(focus.get("todo"), dict):
|
|
526
|
+
lines.append("- none")
|
|
527
|
+
lines.append("")
|
|
528
|
+
return
|
|
529
|
+
todo = cast(dict[str, object], focus["todo"])
|
|
530
|
+
lines.append(f"- id: {todo['id']}")
|
|
531
|
+
lines.append(f"- text: {todo['text']}")
|
|
532
|
+
lines.append(f"- status: {todo['status']}")
|
|
533
|
+
if todo.get("validation_hint"):
|
|
534
|
+
lines.append(f"- validation_hint: {todo['validation_hint']}")
|
|
535
|
+
lines.append("")
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _append_other_todo_summary(lines: list[str], summary: object) -> None:
|
|
539
|
+
lines.extend(["## Other Todo Summary", ""])
|
|
540
|
+
if not isinstance(summary, dict):
|
|
541
|
+
lines.append("- none")
|
|
542
|
+
lines.append("")
|
|
543
|
+
return
|
|
544
|
+
lines.append(f"- total: {summary.get('total', 0)}")
|
|
545
|
+
lines.append(f"- done: {summary.get('done', 0)}")
|
|
546
|
+
lines.append(f"- open: {summary.get('open', 0)}")
|
|
547
|
+
focused_index = summary.get("focused_index")
|
|
548
|
+
if focused_index is not None:
|
|
549
|
+
lines.append(f"- focused_index: {focused_index}")
|
|
550
|
+
lines.append("")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _append_todo_completion_summary(lines: list[str], summary: object) -> None:
|
|
554
|
+
lines.extend(["## Todo Completion Summary", ""])
|
|
555
|
+
if not isinstance(summary, dict):
|
|
556
|
+
lines.append("- none")
|
|
557
|
+
lines.append("")
|
|
558
|
+
return
|
|
559
|
+
lines.append(f"- total: {summary.get('total', 0)}")
|
|
560
|
+
lines.append(f"- done: {summary.get('done', 0)}")
|
|
561
|
+
lines.append(f"- open: {summary.get('open', 0)}")
|
|
562
|
+
lines.append("")
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _append_lock_and_runs(lines: list[str], payload: dict[str, object]) -> None:
|
|
566
|
+
lines.extend(["## Current Run / Lock State", ""])
|
|
567
|
+
runs = payload["runs"]
|
|
568
|
+
assert isinstance(runs, dict)
|
|
569
|
+
latest_impl = runs.get("latest_implementation")
|
|
570
|
+
if isinstance(latest_impl, dict):
|
|
571
|
+
lines.append(
|
|
572
|
+
f"- implementation run: {latest_impl['run_id']} ({latest_impl['status']})"
|
|
573
|
+
)
|
|
574
|
+
else:
|
|
575
|
+
lines.append("- implementation run: none")
|
|
576
|
+
status = payload.get("lock_status")
|
|
577
|
+
if isinstance(status, dict) and status.get("active"):
|
|
578
|
+
lines.append(
|
|
579
|
+
f"- lock: {status.get('stage')} / {status.get('run_id')} "
|
|
580
|
+
f"expired={status.get('expired')}"
|
|
581
|
+
)
|
|
582
|
+
else:
|
|
583
|
+
lines.append("- lock: none")
|
|
584
|
+
lines.append("")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _append_focused_run(lines: list[str], focus: object) -> None:
|
|
588
|
+
lines.extend(["## Focused Run", ""])
|
|
589
|
+
if not isinstance(focus, dict) or not isinstance(focus.get("run"), dict):
|
|
590
|
+
lines.append("- none")
|
|
591
|
+
lines.append("")
|
|
592
|
+
return
|
|
593
|
+
run = cast(dict[str, object], focus["run"])
|
|
594
|
+
lines.append(f"- run_id: {run['run_id']}")
|
|
595
|
+
lines.append(f"- run_type: {run['run_type']}")
|
|
596
|
+
lines.append(f"- status: {run['status']}")
|
|
597
|
+
lines.append(f"- based_on_plan: {run.get('based_on_plan') or 'none'}")
|
|
598
|
+
lines.append(f"- summary: {run.get('summary') or '(no summary)'}")
|
|
599
|
+
lines.append("")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _append_focused_changes(lines: list[str], changes: object) -> None:
|
|
603
|
+
if not isinstance(changes, list):
|
|
604
|
+
return
|
|
605
|
+
lines.extend(["## Focused Changes", ""])
|
|
606
|
+
for item in changes:
|
|
607
|
+
if isinstance(item, dict):
|
|
608
|
+
lines.append(f"- @{item['path']}: {item['summary']}")
|
|
609
|
+
if not changes:
|
|
610
|
+
lines.append("- none")
|
|
611
|
+
lines.append("")
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _append_plan_deviations(lines: list[str], focus: object) -> None:
|
|
615
|
+
lines.extend(["## Plan Deviations", ""])
|
|
616
|
+
if not isinstance(focus, dict) or not isinstance(focus.get("run"), dict):
|
|
617
|
+
lines.append("- none")
|
|
618
|
+
lines.append("")
|
|
619
|
+
return
|
|
620
|
+
run = cast(dict[str, object], focus["run"])
|
|
621
|
+
deviations = run.get("deviations_from_plan")
|
|
622
|
+
if isinstance(deviations, list) and deviations:
|
|
623
|
+
for item in deviations:
|
|
624
|
+
lines.append(f"- {item}")
|
|
625
|
+
else:
|
|
626
|
+
lines.append("- none")
|
|
627
|
+
lines.append("")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _append_todo_updates(lines: list[str], focus: object) -> None:
|
|
631
|
+
lines.extend(["## Todo Updates from that run", ""])
|
|
632
|
+
if not isinstance(focus, dict) or not isinstance(focus.get("run"), dict):
|
|
633
|
+
lines.append("- none")
|
|
634
|
+
lines.append("")
|
|
635
|
+
return
|
|
636
|
+
run = cast(dict[str, object], focus["run"])
|
|
637
|
+
updates = run.get("todo_updates")
|
|
638
|
+
if isinstance(updates, list) and updates:
|
|
639
|
+
for item in updates:
|
|
640
|
+
lines.append(f"- {item}")
|
|
641
|
+
else:
|
|
642
|
+
lines.append("- none")
|
|
643
|
+
lines.append("")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _append_commands_already_run(lines: list[str], focus: object) -> None:
|
|
647
|
+
lines.extend(["## Commands already run", ""])
|
|
648
|
+
if not isinstance(focus, dict) or not isinstance(focus.get("run"), dict):
|
|
649
|
+
lines.append("- none")
|
|
650
|
+
lines.append("")
|
|
651
|
+
return
|
|
652
|
+
run = cast(dict[str, object], focus["run"])
|
|
653
|
+
evidence = run.get("evidence")
|
|
654
|
+
if isinstance(evidence, list) and evidence:
|
|
655
|
+
for item in evidence:
|
|
656
|
+
lines.append(f"- {item}")
|
|
657
|
+
else:
|
|
658
|
+
lines.append("- none")
|
|
659
|
+
lines.append("")
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _append_implementation_summary(lines: list[str], runs: object) -> None:
|
|
663
|
+
if not isinstance(runs, dict):
|
|
664
|
+
return
|
|
665
|
+
latest_impl = runs.get("latest_implementation")
|
|
666
|
+
lines.extend(["## Implementation Summary", ""])
|
|
667
|
+
if isinstance(latest_impl, dict):
|
|
668
|
+
lines.append(str(latest_impl.get("summary") or "(no summary)"))
|
|
669
|
+
else:
|
|
670
|
+
lines.append("(no implementation run)")
|
|
671
|
+
lines.append("")
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _append_change_log(lines: list[str], changes: object) -> None:
|
|
675
|
+
if not isinstance(changes, list):
|
|
676
|
+
return
|
|
677
|
+
lines.extend(["## Code Changes", ""])
|
|
678
|
+
for item in changes:
|
|
679
|
+
if isinstance(item, dict):
|
|
680
|
+
lines.append(f"- @{item['path']}: {item['summary']}")
|
|
681
|
+
if not changes:
|
|
682
|
+
lines.append("- none")
|
|
683
|
+
lines.append("")
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _append_validation_history(lines: list[str], history: object) -> None:
|
|
687
|
+
if not isinstance(history, list):
|
|
688
|
+
return
|
|
689
|
+
lines.extend(["## Previous Validation History", ""])
|
|
690
|
+
for item in history:
|
|
691
|
+
if isinstance(item, dict):
|
|
692
|
+
lines.append(
|
|
693
|
+
f"- {item['run_id']}: "
|
|
694
|
+
f"{item.get('result') or item['status']} — "
|
|
695
|
+
f"{item.get('summary') or ''}"
|
|
696
|
+
)
|
|
697
|
+
if not history:
|
|
698
|
+
lines.append("- none")
|
|
699
|
+
lines.append("")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _append_validation_status(lines: list[str], status_report: object) -> None:
|
|
703
|
+
if not isinstance(status_report, dict):
|
|
704
|
+
return
|
|
705
|
+
|
|
706
|
+
lines.extend(["## Validation Status", ""])
|
|
707
|
+
|
|
708
|
+
can_finish = status_report.get("can_finish_passed", False)
|
|
709
|
+
lines.append(f"**Can Finish Passed:** {'yes' if can_finish else 'no'}")
|
|
710
|
+
lines.append("")
|
|
711
|
+
|
|
712
|
+
criteria = status_report.get("criteria", [])
|
|
713
|
+
if criteria and isinstance(criteria, list):
|
|
714
|
+
lines.append("### Acceptance Criteria")
|
|
715
|
+
for criterion in criteria:
|
|
716
|
+
if isinstance(criterion, dict):
|
|
717
|
+
criterion_id = criterion.get("id", "unknown")
|
|
718
|
+
mandatory = criterion.get("mandatory", False)
|
|
719
|
+
satisfied = criterion.get("satisfied", False)
|
|
720
|
+
latest_status = criterion.get("latest_status", "unknown")
|
|
721
|
+
marker = "pass" if satisfied else "pending"
|
|
722
|
+
mandatory_marker = " (mandatory)" if mandatory else ""
|
|
723
|
+
lines.append(
|
|
724
|
+
f"- {criterion_id}{mandatory_marker}: {latest_status} [{marker}]"
|
|
725
|
+
)
|
|
726
|
+
lines.append("")
|
|
727
|
+
|
|
728
|
+
blockers = status_report.get("blockers", [])
|
|
729
|
+
if blockers and isinstance(blockers, list) and not can_finish:
|
|
730
|
+
lines.append("### Blocking Issues")
|
|
731
|
+
for blocker in blockers:
|
|
732
|
+
if isinstance(blocker, dict):
|
|
733
|
+
kind = blocker.get("kind", "unknown")
|
|
734
|
+
message = blocker.get("message", "")
|
|
735
|
+
lines.append(f"- [{kind}] {message}")
|
|
736
|
+
lines.append("")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _append_required_commands(lines: list[str], accepted_plan: object) -> None:
|
|
740
|
+
lines.extend(["## Required Commands", ""])
|
|
741
|
+
if not isinstance(accepted_plan, dict):
|
|
742
|
+
lines.append("- none")
|
|
743
|
+
lines.append("")
|
|
744
|
+
return
|
|
745
|
+
commands = accepted_plan.get("test_commands")
|
|
746
|
+
if isinstance(commands, list) and commands:
|
|
747
|
+
for item in commands:
|
|
748
|
+
lines.append(f"- {item}")
|
|
749
|
+
else:
|
|
750
|
+
lines.append("- none")
|
|
751
|
+
lines.append("")
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _append_spec_review(lines: list[str]) -> None:
|
|
755
|
+
lines.extend(
|
|
756
|
+
[
|
|
757
|
+
"## Spec Compliance Review",
|
|
758
|
+
"",
|
|
759
|
+
"- Check every acceptance criterion against the focused run evidence.",
|
|
760
|
+
"- Call out deviations_from_plan.",
|
|
761
|
+
"- Mark unclear items when evidence is missing.",
|
|
762
|
+
"",
|
|
763
|
+
]
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _append_code_quality_review(lines: list[str]) -> None:
|
|
768
|
+
lines.extend(
|
|
769
|
+
[
|
|
770
|
+
"## Code Quality Review",
|
|
771
|
+
"",
|
|
772
|
+
"- Check correctness and maintainability risks.",
|
|
773
|
+
"- Call out unsafe or brittle changes.",
|
|
774
|
+
"- Check test coverage gaps.",
|
|
775
|
+
"",
|
|
776
|
+
]
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _append_required_output(lines: list[str], context_for: str) -> None:
|
|
781
|
+
section = {
|
|
782
|
+
"planner": [
|
|
783
|
+
"plan body",
|
|
784
|
+
"assumptions",
|
|
785
|
+
"risks",
|
|
786
|
+
"acceptance criteria",
|
|
787
|
+
"open questions",
|
|
788
|
+
],
|
|
789
|
+
"implementer": [
|
|
790
|
+
"worklog entries",
|
|
791
|
+
"code change records",
|
|
792
|
+
"todo updates",
|
|
793
|
+
"implementation summary",
|
|
794
|
+
],
|
|
795
|
+
"validator": [
|
|
796
|
+
"structured checks",
|
|
797
|
+
"evidence",
|
|
798
|
+
"summary",
|
|
799
|
+
"recommendation",
|
|
800
|
+
],
|
|
801
|
+
"reviewer": ["approval decision"],
|
|
802
|
+
"spec-reviewer": [
|
|
803
|
+
"overall_spec_result: pass|fail|blocked",
|
|
804
|
+
"acceptance_criteria_findings",
|
|
805
|
+
"todo_findings",
|
|
806
|
+
"deviations_from_plan",
|
|
807
|
+
"missing_evidence",
|
|
808
|
+
"recommended_next_action",
|
|
809
|
+
],
|
|
810
|
+
"code-reviewer": [
|
|
811
|
+
"overall_code_quality: pass|fail|blocked",
|
|
812
|
+
"high_risk_issues",
|
|
813
|
+
"maintainability_issues",
|
|
814
|
+
"test_coverage_gaps",
|
|
815
|
+
"unsafe_or_brittle_changes",
|
|
816
|
+
"recommended_next_action",
|
|
817
|
+
],
|
|
818
|
+
"full": ["next action"],
|
|
819
|
+
}[context_for]
|
|
820
|
+
lines.extend(["## Required Output", ""])
|
|
821
|
+
for item in section:
|
|
822
|
+
lines.append(f"- {item}")
|
|
823
|
+
lines.append("")
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _guardrails_for_context_for(context_for: str) -> list[str]:
|
|
827
|
+
if context_for == "planner":
|
|
828
|
+
return [
|
|
829
|
+
"Produce a reviewable plan.",
|
|
830
|
+
"Ask required questions.",
|
|
831
|
+
"Do not implement.",
|
|
832
|
+
]
|
|
833
|
+
if context_for == "implementer":
|
|
834
|
+
return [
|
|
835
|
+
"Implement only the accepted plan and focused todo.",
|
|
836
|
+
"Do not validate.",
|
|
837
|
+
"Log changes.",
|
|
838
|
+
"Mark todo done only with evidence.",
|
|
839
|
+
]
|
|
840
|
+
if context_for == "validator":
|
|
841
|
+
return [
|
|
842
|
+
"Validate against the accepted plan and implementation log.",
|
|
843
|
+
"Record failed validation.",
|
|
844
|
+
"Do not modify implementation.",
|
|
845
|
+
]
|
|
846
|
+
if context_for == "spec-reviewer":
|
|
847
|
+
return [
|
|
848
|
+
"Judge spec compliance only.",
|
|
849
|
+
"Do not rewrite code.",
|
|
850
|
+
"Avoid broad style advice unless it affects compliance.",
|
|
851
|
+
]
|
|
852
|
+
if context_for == "code-reviewer":
|
|
853
|
+
return [
|
|
854
|
+
"Judge maintainability, correctness risks, testing, and safety.",
|
|
855
|
+
"Do not change validation state.",
|
|
856
|
+
"Do not approve task completion.",
|
|
857
|
+
]
|
|
858
|
+
return ["Use this handoff as the source of truth for the next step."]
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _worker_contract(context_for: str) -> tuple[list[str], list[str]]:
|
|
862
|
+
if context_for == "implementer":
|
|
863
|
+
return (
|
|
864
|
+
[
|
|
865
|
+
"implement only the focused todo when one is selected",
|
|
866
|
+
"preserve accepted plan constraints",
|
|
867
|
+
"log implementation changes",
|
|
868
|
+
"mark the focused todo done only with evidence",
|
|
869
|
+
],
|
|
870
|
+
[
|
|
871
|
+
"validate the task",
|
|
872
|
+
"mark unrelated todos done",
|
|
873
|
+
"change the approved plan",
|
|
874
|
+
],
|
|
875
|
+
)
|
|
876
|
+
if context_for == "validator":
|
|
877
|
+
return (
|
|
878
|
+
[
|
|
879
|
+
"validate against the accepted plan and implementation record",
|
|
880
|
+
"record failed and blocked validation explicitly",
|
|
881
|
+
],
|
|
882
|
+
[
|
|
883
|
+
"modify implementation code",
|
|
884
|
+
"approve missing evidence implicitly",
|
|
885
|
+
],
|
|
886
|
+
)
|
|
887
|
+
if context_for == "spec-reviewer":
|
|
888
|
+
return (
|
|
889
|
+
[
|
|
890
|
+
"judge whether the focused run satisfies the plan and "
|
|
891
|
+
"acceptance criteria",
|
|
892
|
+
"cite concrete evidence for pass, fail, or unclear findings",
|
|
893
|
+
],
|
|
894
|
+
[
|
|
895
|
+
"rewrite code",
|
|
896
|
+
"change validation state",
|
|
897
|
+
"give broad style advice unrelated to compliance",
|
|
898
|
+
],
|
|
899
|
+
)
|
|
900
|
+
if context_for == "code-reviewer":
|
|
901
|
+
return (
|
|
902
|
+
[
|
|
903
|
+
"judge maintainability, correctness risk, testing, and safety",
|
|
904
|
+
"cite concrete evidence for risky changes",
|
|
905
|
+
],
|
|
906
|
+
[
|
|
907
|
+
"change validation state",
|
|
908
|
+
"approve task completion",
|
|
909
|
+
],
|
|
910
|
+
)
|
|
911
|
+
if context_for == "planner":
|
|
912
|
+
return (
|
|
913
|
+
[
|
|
914
|
+
"produce a reviewable plan body",
|
|
915
|
+
"surface assumptions and open questions",
|
|
916
|
+
],
|
|
917
|
+
["start implementation"],
|
|
918
|
+
)
|
|
919
|
+
if context_for == "full":
|
|
920
|
+
return (
|
|
921
|
+
["use this dossier as the durable source of truth"],
|
|
922
|
+
["assume chat history"],
|
|
923
|
+
)
|
|
924
|
+
return (
|
|
925
|
+
["use this handoff as the source of truth"],
|
|
926
|
+
["ignore recorded state"],
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
def _resolve_focus(
|
|
931
|
+
workspace_root: Path,
|
|
932
|
+
task_id: str,
|
|
933
|
+
request: ContextRequest,
|
|
934
|
+
todos: list[TaskTodo],
|
|
935
|
+
runs: list[TaskRunRecord],
|
|
936
|
+
changes: list[CodeChangeRecord],
|
|
937
|
+
) -> dict[str, object]:
|
|
938
|
+
focused_todo = None
|
|
939
|
+
focused_run = None
|
|
940
|
+
focused_changes: list[dict[str, object]] = []
|
|
941
|
+
|
|
942
|
+
if request.scope == "todo":
|
|
943
|
+
normalized_todo_id = request.todo_id
|
|
944
|
+
for todo in todos:
|
|
945
|
+
if todo.id == normalized_todo_id:
|
|
946
|
+
focused_todo = todo
|
|
947
|
+
break
|
|
948
|
+
if focused_todo is None:
|
|
949
|
+
raise LaunchError(f"Todo not found: {request.todo_id}")
|
|
950
|
+
elif request.scope == "run":
|
|
951
|
+
assert request.focus_run_id is not None
|
|
952
|
+
focused_run = resolve_run(workspace_root, task_id, request.focus_run_id)
|
|
953
|
+
focused_changes = [
|
|
954
|
+
change.to_dict()
|
|
955
|
+
for change in changes
|
|
956
|
+
if change.implementation_run == focused_run.run_id
|
|
957
|
+
]
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
"todo_id": request.todo_id,
|
|
961
|
+
"todo": focused_todo.to_dict() if focused_todo is not None else None,
|
|
962
|
+
"focus_run_id": request.focus_run_id,
|
|
963
|
+
"run": focused_run.to_dict() if focused_run is not None else None,
|
|
964
|
+
"focused_changes": focused_changes,
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _todo_summary(
|
|
969
|
+
todos: list[TaskTodo], focused_todo_id: str | None
|
|
970
|
+
) -> dict[str, object]:
|
|
971
|
+
focused_index = None
|
|
972
|
+
for index, todo in enumerate(todos, start=1):
|
|
973
|
+
if todo.id == focused_todo_id:
|
|
974
|
+
focused_index = index
|
|
975
|
+
break
|
|
976
|
+
return {
|
|
977
|
+
"total": len(todos),
|
|
978
|
+
"done": sum(1 for todo in todos if todo.done),
|
|
979
|
+
"open": sum(1 for todo in todos if not todo.done and todo.status != "skipped"),
|
|
980
|
+
"focused_index": focused_index,
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _latest_run(runs: list[TaskRunRecord], run_type: str) -> TaskRunRecord | None:
|
|
985
|
+
matches = [item for item in runs if item.run_type == run_type]
|
|
986
|
+
return matches[-1] if matches else None
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _run_to_dict(run: TaskRunRecord | None) -> dict[str, object] | None:
|
|
990
|
+
return run.to_dict() if run is not None else None
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _default_context_for(mode: HandoffMode) -> ContextFor:
|
|
994
|
+
return cast(
|
|
995
|
+
ContextFor,
|
|
996
|
+
{
|
|
997
|
+
"planning": "planner",
|
|
998
|
+
"implementation": "implementer",
|
|
999
|
+
"validation": "validator",
|
|
1000
|
+
"review": "reviewer",
|
|
1001
|
+
"full": "full",
|
|
1002
|
+
}[mode],
|
|
1003
|
+
)
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
def _mode_for_context_for(context_for: ContextFor) -> HandoffMode:
|
|
1007
|
+
return cast(
|
|
1008
|
+
HandoffMode,
|
|
1009
|
+
{
|
|
1010
|
+
"planner": "planning",
|
|
1011
|
+
"implementer": "implementation",
|
|
1012
|
+
"validator": "validation",
|
|
1013
|
+
"reviewer": "review",
|
|
1014
|
+
"spec-reviewer": "review",
|
|
1015
|
+
"code-reviewer": "review",
|
|
1016
|
+
"full": "full",
|
|
1017
|
+
}[context_for],
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _canonical_mode(mode: str | None) -> str:
|
|
1022
|
+
if mode is None:
|
|
1023
|
+
return "full"
|
|
1024
|
+
return {
|
|
1025
|
+
"plan-context": "planning",
|
|
1026
|
+
"implementation-context": "implementation",
|
|
1027
|
+
"validation-context": "validation",
|
|
1028
|
+
"show": "full",
|
|
1029
|
+
}.get(mode, mode)
|