claude-dev-env 2.8.0 → 2.9.0
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.
- package/CLAUDE.md +7 -1
- package/agents/clean-coder.md +9 -19
- package/agents/test_agent_frontmatter.py +26 -0
- package/docs/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +2 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/team-advisor-skill.md +14 -8
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/output-styles/CLAUDE.md +17 -0
- package/output-styles/caveman-agent.md +37 -0
- package/package.json +2 -1
- package/rules/code-standards.md +33 -7
- package/rules/eli11-replies.md +1 -1
- package/scripts/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -0
- package/scripts/spawn_grok_batch.py +553 -9
- package/scripts/test_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_spawn_grok_batch.py +295 -0
- package/skills/CLAUDE.md +4 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
- package/skills/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -0
- package/skills/e-code-review/SKILL.md +6 -1
- package/skills/e-code-review/reference/runner-selection.md +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
- package/skills/e-code-review/scripts/grok_code_review.py +221 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -0
- package/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +6 -3
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Extract patch artifacts and SHA-256 manifests from isolated worktrees.
|
|
3
|
+
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
manifest = write_patch_manifest(
|
|
7
|
+
run_state_directory=run_dir,
|
|
8
|
+
task_id="O-04",
|
|
9
|
+
base_sha="abc",
|
|
10
|
+
worktree_path=worktree,
|
|
11
|
+
worker_report_text="ok",
|
|
12
|
+
)
|
|
13
|
+
ok: manifest carries content hash and changed paths
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import subprocess
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from dev_env_scripts_constants.grok_run_ledger_constants import (
|
|
24
|
+
JSON_INDENT,
|
|
25
|
+
LEDGER_SCHEMA_VERSION,
|
|
26
|
+
PATCH_MANIFEST_FILENAME,
|
|
27
|
+
UTF8_ENCODING,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_sha256_hex(content: bytes) -> str:
|
|
32
|
+
"""Return the hex SHA-256 digest of raw bytes.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
content: Bytes to hash.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Lowercase hex digest string.
|
|
39
|
+
"""
|
|
40
|
+
return hashlib.sha256(content).hexdigest()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def extract_worktree_diff(
|
|
44
|
+
*,
|
|
45
|
+
worktree_path: Path,
|
|
46
|
+
base_sha: str,
|
|
47
|
+
) -> tuple[str, tuple[str, ...]]:
|
|
48
|
+
"""Return unified diff text and changed paths against base_sha.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
worktree_path: Isolated worker worktree.
|
|
52
|
+
base_sha: Base commit SHA the worker started from.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
``(diff_text, changed_paths)``.
|
|
56
|
+
"""
|
|
57
|
+
diff_text = subprocess.check_output(
|
|
58
|
+
["git", "-C", str(worktree_path), "diff", base_sha],
|
|
59
|
+
text=True,
|
|
60
|
+
encoding=UTF8_ENCODING,
|
|
61
|
+
)
|
|
62
|
+
changed_paths_listing = subprocess.check_output(
|
|
63
|
+
["git", "-C", str(worktree_path), "diff", "--name-only", base_sha],
|
|
64
|
+
text=True,
|
|
65
|
+
encoding=UTF8_ENCODING,
|
|
66
|
+
)
|
|
67
|
+
changed_paths = tuple(
|
|
68
|
+
each_line.strip()
|
|
69
|
+
for each_line in changed_paths_listing.splitlines()
|
|
70
|
+
if each_line.strip()
|
|
71
|
+
)
|
|
72
|
+
return diff_text, changed_paths
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_patch_manifest(
|
|
76
|
+
*,
|
|
77
|
+
run_state_directory: Path,
|
|
78
|
+
task_id: str,
|
|
79
|
+
base_sha: str,
|
|
80
|
+
worktree_path: Path,
|
|
81
|
+
worker_report_text: str,
|
|
82
|
+
patch_filename: str | None = None,
|
|
83
|
+
) -> dict[str, object]:
|
|
84
|
+
"""Write a patch file and JSON manifest binding hashes and paths.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
run_state_directory: Directory that holds ledger and patch artifacts.
|
|
88
|
+
task_id: Task the patch belongs to.
|
|
89
|
+
base_sha: Base commit SHA.
|
|
90
|
+
worktree_path: Worker worktree to diff.
|
|
91
|
+
worker_report_text: Worker report body bound into the manifest.
|
|
92
|
+
patch_filename: Optional override for the ``.patch`` filename.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The manifest document written to disk.
|
|
96
|
+
"""
|
|
97
|
+
run_state_directory = Path(run_state_directory)
|
|
98
|
+
run_state_directory.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
diff_text, changed_paths = extract_worktree_diff(
|
|
100
|
+
worktree_path=Path(worktree_path),
|
|
101
|
+
base_sha=base_sha,
|
|
102
|
+
)
|
|
103
|
+
patch_name = patch_filename or f"{task_id}.patch"
|
|
104
|
+
patch_path = run_state_directory / patch_name
|
|
105
|
+
patch_bytes = diff_text.encode(UTF8_ENCODING)
|
|
106
|
+
patch_path.write_bytes(patch_bytes)
|
|
107
|
+
content_hash = compute_sha256_hex(patch_bytes)
|
|
108
|
+
report_hash = compute_sha256_hex(worker_report_text.encode(UTF8_ENCODING))
|
|
109
|
+
manifest = {
|
|
110
|
+
"schema_version": LEDGER_SCHEMA_VERSION,
|
|
111
|
+
"task_id": task_id,
|
|
112
|
+
"base_sha": base_sha,
|
|
113
|
+
"changed_paths": list(changed_paths),
|
|
114
|
+
"patch_path": str(patch_path),
|
|
115
|
+
"content_sha256": content_hash,
|
|
116
|
+
"worker_report_sha256": report_hash,
|
|
117
|
+
}
|
|
118
|
+
manifest_path = run_state_directory / PATCH_MANIFEST_FILENAME
|
|
119
|
+
manifest_path.write_text(
|
|
120
|
+
json.dumps(manifest, indent=JSON_INDENT) + "\n",
|
|
121
|
+
encoding=UTF8_ENCODING,
|
|
122
|
+
)
|
|
123
|
+
return manifest
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Host-neutral file-backed ledger for Grok orchestration task state.
|
|
3
|
+
|
|
4
|
+
Records every delegated unit before dispatch, enforces one live owner and one
|
|
5
|
+
unique advisor session per in-progress task, blocks on unfinished dependencies,
|
|
6
|
+
and reopens tasks when the base snapshot drifts.
|
|
7
|
+
|
|
8
|
+
::
|
|
9
|
+
|
|
10
|
+
ledger = GrokRunLedger(run_state_directory)
|
|
11
|
+
ledger.register_task(task_id="O-04", dependencies=())
|
|
12
|
+
ledger.mark_in_progress(
|
|
13
|
+
task_id="O-04",
|
|
14
|
+
owner_id="worker-1",
|
|
15
|
+
advisor_session_id="sess-1",
|
|
16
|
+
base_sha="abc",
|
|
17
|
+
)
|
|
18
|
+
ok: task status becomes in_progress with that owner and session
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import tempfile
|
|
26
|
+
from dataclasses import asdict, dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from dev_env_scripts_constants.grok_run_ledger_constants import (
|
|
30
|
+
ALL_LEGAL_TASK_STATUSES,
|
|
31
|
+
JSON_INDENT,
|
|
32
|
+
LEDGER_FILENAME,
|
|
33
|
+
LEDGER_SCHEMA_VERSION,
|
|
34
|
+
TASK_STATUS_ADVISOR_BLOCKED,
|
|
35
|
+
TASK_STATUS_COMPLETED,
|
|
36
|
+
TASK_STATUS_IN_PROGRESS,
|
|
37
|
+
TASK_STATUS_PENDING,
|
|
38
|
+
TASK_STATUS_PENDING_REVIEW,
|
|
39
|
+
TEMPORARY_LEDGER_PREFIX,
|
|
40
|
+
TEMPORARY_LEDGER_SUFFIX,
|
|
41
|
+
UTF8_ENCODING,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class LedgerTaskRecord:
|
|
47
|
+
"""One delegated unit tracked in the run ledger."""
|
|
48
|
+
|
|
49
|
+
task_id: str
|
|
50
|
+
status: str = TASK_STATUS_PENDING
|
|
51
|
+
dependencies: tuple[str, ...] = ()
|
|
52
|
+
owner_id: str | None = None
|
|
53
|
+
advisor_session_id: str | None = None
|
|
54
|
+
advisor_verdict: str | None = None
|
|
55
|
+
base_sha: str | None = None
|
|
56
|
+
reviewed_head: str | None = None
|
|
57
|
+
changed_paths: tuple[str, ...] = ()
|
|
58
|
+
acceptance_mapping: dict[str, str] = field(default_factory=dict)
|
|
59
|
+
test_evidence: list[str] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GrokRunLedger:
|
|
63
|
+
"""Atomic file-backed ledger: register_task, get_task, can_dispatch, mark_in_progress, mark_completed, mark_advisor_blocked, invalidate_on_snapshot_drift, all_tasks."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, run_state_directory: Path) -> None:
|
|
66
|
+
self.run_state_directory = Path(run_state_directory)
|
|
67
|
+
self.ledger_path = self.run_state_directory / LEDGER_FILENAME
|
|
68
|
+
self._task_by_id: dict[str, LedgerTaskRecord] = {}
|
|
69
|
+
self.run_state_directory.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
if self.ledger_path.is_file():
|
|
71
|
+
self._load()
|
|
72
|
+
|
|
73
|
+
def _load(self) -> None:
|
|
74
|
+
payload = json.loads(self.ledger_path.read_text(encoding=UTF8_ENCODING))
|
|
75
|
+
for each_task in payload.get("tasks", []):
|
|
76
|
+
record = LedgerTaskRecord(
|
|
77
|
+
task_id=each_task["task_id"],
|
|
78
|
+
status=each_task["status"],
|
|
79
|
+
dependencies=tuple(each_task.get("dependencies") or ()),
|
|
80
|
+
owner_id=each_task.get("owner_id"),
|
|
81
|
+
advisor_session_id=each_task.get("advisor_session_id"),
|
|
82
|
+
advisor_verdict=each_task.get("advisor_verdict"),
|
|
83
|
+
base_sha=each_task.get("base_sha"),
|
|
84
|
+
reviewed_head=each_task.get("reviewed_head"),
|
|
85
|
+
changed_paths=tuple(each_task.get("changed_paths") or ()),
|
|
86
|
+
acceptance_mapping=dict(each_task.get("acceptance_mapping") or {}),
|
|
87
|
+
test_evidence=list(each_task.get("test_evidence") or []),
|
|
88
|
+
)
|
|
89
|
+
self._task_by_id[record.task_id] = record
|
|
90
|
+
|
|
91
|
+
def _atomic_write(self) -> None:
|
|
92
|
+
document = {
|
|
93
|
+
"schema_version": LEDGER_SCHEMA_VERSION,
|
|
94
|
+
"tasks": [
|
|
95
|
+
{
|
|
96
|
+
**asdict(each_record),
|
|
97
|
+
"dependencies": list(each_record.dependencies),
|
|
98
|
+
"changed_paths": list(each_record.changed_paths),
|
|
99
|
+
}
|
|
100
|
+
for each_record in sorted(
|
|
101
|
+
self._task_by_id.values(), key=lambda item: item.task_id
|
|
102
|
+
)
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
encoded = json.dumps(document, indent=JSON_INDENT) + "\n"
|
|
106
|
+
file_descriptor, temporary_path = tempfile.mkstemp(
|
|
107
|
+
dir=self.run_state_directory,
|
|
108
|
+
prefix=TEMPORARY_LEDGER_PREFIX,
|
|
109
|
+
suffix=TEMPORARY_LEDGER_SUFFIX,
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
with os.fdopen(file_descriptor, "w", encoding=UTF8_ENCODING) as handle:
|
|
113
|
+
handle.write(encoded)
|
|
114
|
+
handle.flush()
|
|
115
|
+
os.fsync(handle.fileno())
|
|
116
|
+
os.replace(temporary_path, self.ledger_path)
|
|
117
|
+
except (OSError, TypeError, ValueError):
|
|
118
|
+
if os.path.exists(temporary_path):
|
|
119
|
+
os.unlink(temporary_path)
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
def register_task(
|
|
123
|
+
self,
|
|
124
|
+
*,
|
|
125
|
+
task_id: str,
|
|
126
|
+
all_dependencies: tuple[str, ...] = (),
|
|
127
|
+
) -> LedgerTaskRecord:
|
|
128
|
+
"""Record a delegated unit before dispatch.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
task_id: Stable task identifier.
|
|
132
|
+
all_dependencies: Task ids that must complete before dispatch.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The registered pending task record.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: When the task id already exists.
|
|
139
|
+
"""
|
|
140
|
+
if task_id in self._task_by_id:
|
|
141
|
+
raise ValueError(f"task already registered: {task_id}")
|
|
142
|
+
record = LedgerTaskRecord(
|
|
143
|
+
task_id=task_id,
|
|
144
|
+
status=TASK_STATUS_PENDING,
|
|
145
|
+
dependencies=all_dependencies,
|
|
146
|
+
)
|
|
147
|
+
self._task_by_id[task_id] = record
|
|
148
|
+
self._atomic_write()
|
|
149
|
+
return record
|
|
150
|
+
|
|
151
|
+
def get_task(self, task_id: str) -> LedgerTaskRecord:
|
|
152
|
+
"""Return one task record.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
task_id: Stable task identifier.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The stored task record.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
KeyError: When the task id is unknown.
|
|
162
|
+
"""
|
|
163
|
+
return self._task_by_id[task_id]
|
|
164
|
+
|
|
165
|
+
def can_dispatch(self, task_id: str) -> bool:
|
|
166
|
+
"""Return whether every dependency has completed successfully.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
task_id: Task to evaluate.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True when every dependency is ``completed``.
|
|
173
|
+
"""
|
|
174
|
+
record = self.get_task(task_id)
|
|
175
|
+
for each_dependency in record.dependencies:
|
|
176
|
+
dependency_record = self._task_by_id.get(each_dependency)
|
|
177
|
+
if dependency_record is None:
|
|
178
|
+
return False
|
|
179
|
+
if dependency_record.status != TASK_STATUS_COMPLETED:
|
|
180
|
+
return False
|
|
181
|
+
return True
|
|
182
|
+
|
|
183
|
+
def mark_in_progress(
|
|
184
|
+
self,
|
|
185
|
+
*,
|
|
186
|
+
task_id: str,
|
|
187
|
+
owner_id: str,
|
|
188
|
+
advisor_session_id: str,
|
|
189
|
+
base_sha: str,
|
|
190
|
+
) -> LedgerTaskRecord:
|
|
191
|
+
"""Assign one owner and unique advisor session and start the task.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
task_id: Task to start.
|
|
195
|
+
owner_id: Live owner identity.
|
|
196
|
+
advisor_session_id: Unique advisor session for this worker.
|
|
197
|
+
base_sha: Snapshot SHA at dispatch.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The updated task record.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: When dispatch is blocked, another owner is live, the
|
|
204
|
+
advisor session is reused, or status is illegal.
|
|
205
|
+
"""
|
|
206
|
+
if not self.can_dispatch(task_id):
|
|
207
|
+
raise ValueError(f"dependencies incomplete for {task_id}")
|
|
208
|
+
for each_record in self._task_by_id.values():
|
|
209
|
+
if (
|
|
210
|
+
each_record.status == TASK_STATUS_IN_PROGRESS
|
|
211
|
+
and each_record.owner_id == owner_id
|
|
212
|
+
and each_record.task_id != task_id
|
|
213
|
+
):
|
|
214
|
+
raise ValueError(f"owner already live on {each_record.task_id}")
|
|
215
|
+
if (
|
|
216
|
+
each_record.advisor_session_id == advisor_session_id
|
|
217
|
+
and each_record.task_id != task_id
|
|
218
|
+
):
|
|
219
|
+
raise ValueError(
|
|
220
|
+
f"advisor session already bound to {each_record.task_id}"
|
|
221
|
+
)
|
|
222
|
+
record = self.get_task(task_id)
|
|
223
|
+
if record.status not in {TASK_STATUS_PENDING, TASK_STATUS_PENDING_REVIEW}:
|
|
224
|
+
raise ValueError(f"cannot start task from status {record.status}")
|
|
225
|
+
record.status = TASK_STATUS_IN_PROGRESS
|
|
226
|
+
record.owner_id = owner_id
|
|
227
|
+
record.advisor_session_id = advisor_session_id
|
|
228
|
+
record.base_sha = base_sha
|
|
229
|
+
self._atomic_write()
|
|
230
|
+
return record
|
|
231
|
+
|
|
232
|
+
def mark_completed(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
task_id: str,
|
|
236
|
+
reviewed_head: str,
|
|
237
|
+
all_changed_paths: tuple[str, ...],
|
|
238
|
+
advisor_verdict: str,
|
|
239
|
+
all_acceptance_mapping: dict[str, str],
|
|
240
|
+
all_test_evidence: list[str],
|
|
241
|
+
) -> LedgerTaskRecord:
|
|
242
|
+
"""Record successful terminal state for an in-progress task.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
task_id: Task to complete.
|
|
246
|
+
reviewed_head: Final reviewed commit SHA.
|
|
247
|
+
all_changed_paths: Paths changed by the worker.
|
|
248
|
+
advisor_verdict: Opening advisor signal (for example ENDORSE).
|
|
249
|
+
all_acceptance_mapping: Acceptance criterion to evidence map.
|
|
250
|
+
all_test_evidence: Commands or artifacts proving tests.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The completed task record.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
ValueError: When the task is not in progress.
|
|
257
|
+
"""
|
|
258
|
+
record = self.get_task(task_id)
|
|
259
|
+
if record.status != TASK_STATUS_IN_PROGRESS:
|
|
260
|
+
raise ValueError(f"cannot complete task from status {record.status}")
|
|
261
|
+
record.status = TASK_STATUS_COMPLETED
|
|
262
|
+
record.reviewed_head = reviewed_head
|
|
263
|
+
record.changed_paths = all_changed_paths
|
|
264
|
+
record.advisor_verdict = advisor_verdict
|
|
265
|
+
record.acceptance_mapping = dict(all_acceptance_mapping)
|
|
266
|
+
record.test_evidence = list(all_test_evidence)
|
|
267
|
+
self._atomic_write()
|
|
268
|
+
return record
|
|
269
|
+
|
|
270
|
+
def mark_advisor_blocked(self, *, task_id: str, reason: str) -> LedgerTaskRecord:
|
|
271
|
+
"""Stop a task because the advisor path failed closed.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
task_id: Task to block.
|
|
275
|
+
reason: Short failure reason stored in test evidence.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
The blocked task record.
|
|
279
|
+
"""
|
|
280
|
+
record = self.get_task(task_id)
|
|
281
|
+
record.status = TASK_STATUS_ADVISOR_BLOCKED
|
|
282
|
+
record.test_evidence = [reason]
|
|
283
|
+
self._atomic_write()
|
|
284
|
+
return record
|
|
285
|
+
|
|
286
|
+
def invalidate_on_snapshot_drift(
|
|
287
|
+
self,
|
|
288
|
+
*,
|
|
289
|
+
task_id: str,
|
|
290
|
+
current_sha: str,
|
|
291
|
+
) -> LedgerTaskRecord:
|
|
292
|
+
"""Move a task back to pending review when the base snapshot drifts.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
task_id: Task to invalidate.
|
|
296
|
+
current_sha: Live SHA compared to the recorded base.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The updated task record (unchanged when SHAs match).
|
|
300
|
+
"""
|
|
301
|
+
record = self.get_task(task_id)
|
|
302
|
+
if record.base_sha is None or record.base_sha == current_sha:
|
|
303
|
+
return record
|
|
304
|
+
record.status = TASK_STATUS_PENDING_REVIEW
|
|
305
|
+
record.owner_id = None
|
|
306
|
+
self._atomic_write()
|
|
307
|
+
return record
|
|
308
|
+
|
|
309
|
+
def all_tasks(self) -> tuple[LedgerTaskRecord, ...]:
|
|
310
|
+
"""Return every task record sorted by task id."""
|
|
311
|
+
return tuple(
|
|
312
|
+
sorted(self._task_by_id.values(), key=lambda item: item.task_id)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def is_legal_status(status: str) -> bool:
|
|
317
|
+
"""Return whether a status token is in the legal set."""
|
|
318
|
+
return status in ALL_LEGAL_TASK_STATUSES
|