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
taskledger/search.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from taskledger.errors import LaunchError
|
|
12
|
+
from taskledger.storage.paths import ProjectPaths
|
|
13
|
+
from taskledger.storage.repos import (
|
|
14
|
+
ProjectRepo,
|
|
15
|
+
load_repos,
|
|
16
|
+
resolve_repo,
|
|
17
|
+
resolve_repo_root,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
SearchMatchKind = Literal["path", "content", "symbol"]
|
|
21
|
+
|
|
22
|
+
_TEXT_EXTENSIONS = {
|
|
23
|
+
".csv",
|
|
24
|
+
".html",
|
|
25
|
+
".js",
|
|
26
|
+
".json",
|
|
27
|
+
".md",
|
|
28
|
+
".po",
|
|
29
|
+
".py",
|
|
30
|
+
".rst",
|
|
31
|
+
".scss",
|
|
32
|
+
".sql",
|
|
33
|
+
".toml",
|
|
34
|
+
".ts",
|
|
35
|
+
".txt",
|
|
36
|
+
".xml",
|
|
37
|
+
".yaml",
|
|
38
|
+
".yml",
|
|
39
|
+
}
|
|
40
|
+
_SKIP_DIRS = {".git", ".hg", ".svn", ".venv", "__pycache__", "node_modules"}
|
|
41
|
+
_SYMBOL_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
|
|
42
|
+
(
|
|
43
|
+
"python",
|
|
44
|
+
re.compile(r"^\s*(?:def|class)\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"javascript",
|
|
48
|
+
re.compile(
|
|
49
|
+
r"^\s*(?:function|class)\s+([A-Za-z_$][A-Za-z0-9_$]*)", re.MULTILINE
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
("xml-id", re.compile(r"""\bid=["']([^"']+)["']""")),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True, frozen=True)
|
|
57
|
+
class ProjectSearchMatch:
|
|
58
|
+
repo: str
|
|
59
|
+
path: str
|
|
60
|
+
kind: SearchMatchKind
|
|
61
|
+
line: int | None
|
|
62
|
+
text: str
|
|
63
|
+
symbol: str | None = None
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict[str, object]:
|
|
66
|
+
return {
|
|
67
|
+
"repo": self.repo,
|
|
68
|
+
"path": self.path,
|
|
69
|
+
"kind": self.kind,
|
|
70
|
+
"line": self.line,
|
|
71
|
+
"text": self.text,
|
|
72
|
+
"symbol": self.symbol,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True, frozen=True)
|
|
77
|
+
class ProjectDependencyInfo:
|
|
78
|
+
repo: str
|
|
79
|
+
module: str
|
|
80
|
+
manifest_path: str
|
|
81
|
+
depends: tuple[str, ...]
|
|
82
|
+
module_name: str | None
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> dict[str, object]:
|
|
85
|
+
return {
|
|
86
|
+
"repo": self.repo,
|
|
87
|
+
"module": self.module,
|
|
88
|
+
"manifest_path": self.manifest_path,
|
|
89
|
+
"depends": list(self.depends),
|
|
90
|
+
"module_name": self.module_name,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def search_project(
|
|
95
|
+
paths: ProjectPaths,
|
|
96
|
+
*,
|
|
97
|
+
query: str,
|
|
98
|
+
repo_refs: tuple[str, ...] = (),
|
|
99
|
+
limit: int = 50,
|
|
100
|
+
) -> list[ProjectSearchMatch]:
|
|
101
|
+
lowered_query = query.strip().lower()
|
|
102
|
+
if not lowered_query:
|
|
103
|
+
raise LaunchError("Project search query must not be empty.")
|
|
104
|
+
matches: list[ProjectSearchMatch] = []
|
|
105
|
+
for repo, file_path, relative_path in iter_repo_files(paths, repo_refs=repo_refs):
|
|
106
|
+
path_text = relative_path.as_posix()
|
|
107
|
+
if lowered_query in path_text.lower():
|
|
108
|
+
matches.append(
|
|
109
|
+
ProjectSearchMatch(
|
|
110
|
+
repo=repo.name,
|
|
111
|
+
path=path_text,
|
|
112
|
+
kind="path",
|
|
113
|
+
line=None,
|
|
114
|
+
text=path_text,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
if len(matches) >= limit:
|
|
118
|
+
break
|
|
119
|
+
content = _read_text_file(file_path)
|
|
120
|
+
if content is None:
|
|
121
|
+
continue
|
|
122
|
+
content_matches = 0
|
|
123
|
+
for index, line in enumerate(content.splitlines(), start=1):
|
|
124
|
+
if lowered_query not in line.lower():
|
|
125
|
+
continue
|
|
126
|
+
matches.append(
|
|
127
|
+
ProjectSearchMatch(
|
|
128
|
+
repo=repo.name,
|
|
129
|
+
path=path_text,
|
|
130
|
+
kind="content",
|
|
131
|
+
line=index,
|
|
132
|
+
text=line.strip(),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
content_matches += 1
|
|
136
|
+
if len(matches) >= limit or content_matches >= 3:
|
|
137
|
+
break
|
|
138
|
+
if len(matches) >= limit:
|
|
139
|
+
break
|
|
140
|
+
return matches
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def grep_project(
|
|
144
|
+
paths: ProjectPaths,
|
|
145
|
+
*,
|
|
146
|
+
pattern: str,
|
|
147
|
+
repo_refs: tuple[str, ...] = (),
|
|
148
|
+
limit: int = 100,
|
|
149
|
+
) -> list[ProjectSearchMatch]:
|
|
150
|
+
if not pattern.strip():
|
|
151
|
+
raise LaunchError("Project grep pattern must not be empty.")
|
|
152
|
+
try:
|
|
153
|
+
compiled = re.compile(pattern)
|
|
154
|
+
except re.error as exc:
|
|
155
|
+
raise LaunchError(f"Invalid project grep pattern: {exc}") from exc
|
|
156
|
+
matches: list[ProjectSearchMatch] = []
|
|
157
|
+
for repo, file_path, relative_path in iter_repo_files(paths, repo_refs=repo_refs):
|
|
158
|
+
content = _read_text_file(file_path)
|
|
159
|
+
if content is None:
|
|
160
|
+
continue
|
|
161
|
+
for index, line in enumerate(content.splitlines(), start=1):
|
|
162
|
+
if compiled.search(line) is None:
|
|
163
|
+
continue
|
|
164
|
+
matches.append(
|
|
165
|
+
ProjectSearchMatch(
|
|
166
|
+
repo=repo.name,
|
|
167
|
+
path=relative_path.as_posix(),
|
|
168
|
+
kind="content",
|
|
169
|
+
line=index,
|
|
170
|
+
text=line.strip(),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
if len(matches) >= limit:
|
|
174
|
+
return matches
|
|
175
|
+
return matches
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def symbols_project(
|
|
179
|
+
paths: ProjectPaths,
|
|
180
|
+
*,
|
|
181
|
+
query: str,
|
|
182
|
+
repo_refs: tuple[str, ...] = (),
|
|
183
|
+
limit: int = 50,
|
|
184
|
+
) -> list[ProjectSearchMatch]:
|
|
185
|
+
lowered_query = query.strip().lower()
|
|
186
|
+
if not lowered_query:
|
|
187
|
+
raise LaunchError("Project symbol query must not be empty.")
|
|
188
|
+
matches: list[ProjectSearchMatch] = []
|
|
189
|
+
for repo, file_path, relative_path in iter_repo_files(paths, repo_refs=repo_refs):
|
|
190
|
+
content = _read_text_file(file_path)
|
|
191
|
+
if content is None:
|
|
192
|
+
continue
|
|
193
|
+
for _, pattern in _SYMBOL_PATTERNS:
|
|
194
|
+
for match in pattern.finditer(content):
|
|
195
|
+
symbol = match.group(1)
|
|
196
|
+
if lowered_query not in symbol.lower():
|
|
197
|
+
continue
|
|
198
|
+
line = content.count("\n", 0, match.start()) + 1
|
|
199
|
+
matches.append(
|
|
200
|
+
ProjectSearchMatch(
|
|
201
|
+
repo=repo.name,
|
|
202
|
+
path=relative_path.as_posix(),
|
|
203
|
+
kind="symbol",
|
|
204
|
+
line=line,
|
|
205
|
+
text=symbol,
|
|
206
|
+
symbol=symbol,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
if len(matches) >= limit:
|
|
210
|
+
return matches
|
|
211
|
+
return matches
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def module_dependencies(
|
|
215
|
+
paths: ProjectPaths,
|
|
216
|
+
*,
|
|
217
|
+
repo_ref: str,
|
|
218
|
+
module: str,
|
|
219
|
+
) -> ProjectDependencyInfo:
|
|
220
|
+
repo = resolve_repo(paths, repo_ref)
|
|
221
|
+
repo_root = resolve_repo_root(paths, repo.name)
|
|
222
|
+
manifest_path = _find_module_manifest(repo_root, module)
|
|
223
|
+
if manifest_path is None:
|
|
224
|
+
raise LaunchError(f"Could not find module '{module}' in repo {repo.name}.")
|
|
225
|
+
try:
|
|
226
|
+
payload = ast.literal_eval(manifest_path.read_text(encoding="utf-8"))
|
|
227
|
+
except (OSError, SyntaxError, ValueError) as exc:
|
|
228
|
+
raise LaunchError(f"Failed to parse manifest {manifest_path}: {exc}") from exc
|
|
229
|
+
if not isinstance(payload, dict):
|
|
230
|
+
raise LaunchError(f"Manifest {manifest_path} must contain a dict literal.")
|
|
231
|
+
depends = payload.get("depends", [])
|
|
232
|
+
if not isinstance(depends, list) or not all(
|
|
233
|
+
isinstance(item, str) for item in depends
|
|
234
|
+
):
|
|
235
|
+
raise LaunchError(f"Manifest {manifest_path} has an invalid depends list.")
|
|
236
|
+
module_name = payload.get("name")
|
|
237
|
+
if module_name is not None and not isinstance(module_name, str):
|
|
238
|
+
raise LaunchError(f"Manifest {manifest_path} has an invalid module name.")
|
|
239
|
+
return ProjectDependencyInfo(
|
|
240
|
+
repo=repo.name,
|
|
241
|
+
module=module,
|
|
242
|
+
manifest_path=str(manifest_path.relative_to(repo_root)),
|
|
243
|
+
depends=tuple(depends),
|
|
244
|
+
module_name=module_name,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def iter_repo_files(
|
|
249
|
+
paths: ProjectPaths, *, repo_refs: tuple[str, ...] = ()
|
|
250
|
+
) -> Iterable[tuple[ProjectRepo, Path, Path]]:
|
|
251
|
+
repos = _selected_repos(paths, repo_refs)
|
|
252
|
+
for repo in repos:
|
|
253
|
+
repo_root = resolve_repo_root(paths, repo.name)
|
|
254
|
+
for root, dirnames, filenames in os.walk(repo_root):
|
|
255
|
+
dirnames[:] = [
|
|
256
|
+
item
|
|
257
|
+
for item in dirnames
|
|
258
|
+
if item not in _SKIP_DIRS and not item.startswith(".")
|
|
259
|
+
]
|
|
260
|
+
for filename in filenames:
|
|
261
|
+
file_path = Path(root) / filename
|
|
262
|
+
if file_path.suffix and file_path.suffix not in _TEXT_EXTENSIONS:
|
|
263
|
+
continue
|
|
264
|
+
yield repo, file_path, file_path.relative_to(repo_root)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def discover_relevant_files(
|
|
268
|
+
paths: ProjectPaths,
|
|
269
|
+
*,
|
|
270
|
+
query: str,
|
|
271
|
+
repo_refs: tuple[str, ...] = (),
|
|
272
|
+
limit: int = 12,
|
|
273
|
+
) -> tuple[str, ...]:
|
|
274
|
+
tokens = _discovery_tokens(query)
|
|
275
|
+
if not tokens:
|
|
276
|
+
raise LaunchError("Project discovery query must contain useful search terms.")
|
|
277
|
+
scored: list[tuple[int, str]] = []
|
|
278
|
+
for repo, file_path, relative_path in iter_repo_files(paths, repo_refs=repo_refs):
|
|
279
|
+
path_text = relative_path.as_posix().lower()
|
|
280
|
+
content = _read_text_file(file_path)
|
|
281
|
+
lowered_content = content.lower() if content is not None else ""
|
|
282
|
+
score = 0
|
|
283
|
+
for token in tokens:
|
|
284
|
+
if token in path_text:
|
|
285
|
+
score += 4
|
|
286
|
+
if lowered_content and token in lowered_content:
|
|
287
|
+
score += 1
|
|
288
|
+
if relative_path.name == "__manifest__.py":
|
|
289
|
+
score += 1
|
|
290
|
+
if score <= 0:
|
|
291
|
+
continue
|
|
292
|
+
scored.append((score, f"{repo.name}:{relative_path.as_posix()}"))
|
|
293
|
+
scored.sort(key=lambda item: (-item[0], item[1]))
|
|
294
|
+
return tuple(item[1] for item in scored[:limit])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _selected_repos(
|
|
298
|
+
paths: ProjectPaths, repo_refs: tuple[str, ...]
|
|
299
|
+
) -> list[ProjectRepo]:
|
|
300
|
+
if repo_refs:
|
|
301
|
+
return [resolve_repo(paths, ref) for ref in repo_refs]
|
|
302
|
+
repos = load_repos(paths)
|
|
303
|
+
if not repos:
|
|
304
|
+
raise LaunchError("No project repos are registered.")
|
|
305
|
+
return repos
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _read_text_file(path: Path) -> str | None:
|
|
309
|
+
try:
|
|
310
|
+
return path.read_text(encoding="utf-8")
|
|
311
|
+
except UnicodeDecodeError:
|
|
312
|
+
return None
|
|
313
|
+
except OSError as exc:
|
|
314
|
+
raise LaunchError(f"Failed to read {path}: {exc}") from exc
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _find_module_manifest(repo_root: Path, module: str) -> Path | None:
|
|
318
|
+
for root, dirnames, filenames in os.walk(repo_root):
|
|
319
|
+
dirnames[:] = [
|
|
320
|
+
item
|
|
321
|
+
for item in dirnames
|
|
322
|
+
if item not in _SKIP_DIRS and not item.startswith(".")
|
|
323
|
+
]
|
|
324
|
+
if "__manifest__.py" not in filenames:
|
|
325
|
+
continue
|
|
326
|
+
candidate = Path(root)
|
|
327
|
+
if candidate.name == module:
|
|
328
|
+
return candidate / "__manifest__.py"
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _discovery_tokens(query: str) -> tuple[str, ...]:
|
|
333
|
+
stop_words = {
|
|
334
|
+
"and",
|
|
335
|
+
"for",
|
|
336
|
+
"from",
|
|
337
|
+
"have",
|
|
338
|
+
"into",
|
|
339
|
+
"that",
|
|
340
|
+
"the",
|
|
341
|
+
"this",
|
|
342
|
+
"with",
|
|
343
|
+
}
|
|
344
|
+
tokens: list[str] = []
|
|
345
|
+
for token in re.findall(r"[a-zA-Z0-9_]+", query.lower()):
|
|
346
|
+
if len(token) < 4 or token in stop_words or token in tokens:
|
|
347
|
+
continue
|
|
348
|
+
tokens.append(token)
|
|
349
|
+
return tuple(tokens[:16])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""State-oriented taskledger services."""
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Actor and harness identity resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import socket
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from taskledger.domain.models import ActorRef, HarnessRef
|
|
12
|
+
from taskledger.domain.states import (
|
|
13
|
+
normalize_actor_role,
|
|
14
|
+
normalize_actor_type,
|
|
15
|
+
normalize_harness_kind,
|
|
16
|
+
)
|
|
17
|
+
from taskledger.ids import next_project_id
|
|
18
|
+
from taskledger.storage.task_store import load_actor_state, load_harness_state
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_actor(
|
|
22
|
+
*,
|
|
23
|
+
actor_type: str | None = None,
|
|
24
|
+
actor_name: str | None = None,
|
|
25
|
+
role: str | None = None,
|
|
26
|
+
tool: str | None = None,
|
|
27
|
+
session_id: str | None = None,
|
|
28
|
+
harness_id: str | None = None,
|
|
29
|
+
workspace_root: Path | None = None,
|
|
30
|
+
) -> ActorRef:
|
|
31
|
+
"""
|
|
32
|
+
Resolve actor identity with fallback order:
|
|
33
|
+
1. Explicit parameters
|
|
34
|
+
2. Environment variables
|
|
35
|
+
3. Stored state (actor.yaml)
|
|
36
|
+
4. Auto-detect from environment
|
|
37
|
+
5. Safe default
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# 1. Use explicit params if provided
|
|
41
|
+
if actor_type and actor_name:
|
|
42
|
+
return ActorRef(
|
|
43
|
+
actor_type=normalize_actor_type(actor_type),
|
|
44
|
+
actor_name=actor_name,
|
|
45
|
+
role=normalize_actor_role(role) if role else None,
|
|
46
|
+
tool=tool,
|
|
47
|
+
session_id=session_id,
|
|
48
|
+
harness_id=harness_id,
|
|
49
|
+
host=socket.gethostname(),
|
|
50
|
+
pid=os.getpid(),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 2. Check environment variables
|
|
54
|
+
env_actor_type = os.getenv("TASKLEDGER_ACTOR_TYPE")
|
|
55
|
+
env_actor_name = os.getenv("TASKLEDGER_ACTOR_NAME")
|
|
56
|
+
env_role = os.getenv("TASKLEDGER_ACTOR_ROLE")
|
|
57
|
+
env_harness = os.getenv("TASKLEDGER_HARNESS")
|
|
58
|
+
env_session_id = os.getenv("TASKLEDGER_SESSION_ID")
|
|
59
|
+
resolved_role = env_role or role
|
|
60
|
+
|
|
61
|
+
if env_actor_type or env_actor_name:
|
|
62
|
+
return ActorRef(
|
|
63
|
+
actor_type=normalize_actor_type(env_actor_type or actor_type or "agent"),
|
|
64
|
+
actor_name=env_actor_name or actor_name or "taskledger",
|
|
65
|
+
role=normalize_actor_role(resolved_role) if resolved_role else None,
|
|
66
|
+
tool=tool,
|
|
67
|
+
session_id=env_session_id or session_id,
|
|
68
|
+
harness_id=harness_id or env_harness,
|
|
69
|
+
host=socket.gethostname(),
|
|
70
|
+
pid=os.getpid(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 3. Check stored state
|
|
74
|
+
if workspace_root is not None:
|
|
75
|
+
stored = load_actor_state(workspace_root)
|
|
76
|
+
if stored is not None:
|
|
77
|
+
return ActorRef(
|
|
78
|
+
actor_type=stored.actor_type,
|
|
79
|
+
actor_name=stored.actor_name,
|
|
80
|
+
role=stored.role,
|
|
81
|
+
tool=stored.tool or tool,
|
|
82
|
+
session_id=stored.session_id or session_id,
|
|
83
|
+
harness_id=harness_id,
|
|
84
|
+
host=socket.gethostname(),
|
|
85
|
+
pid=os.getpid(),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 4. Auto-detect from environment
|
|
89
|
+
if os.getenv("OPENCODE_VERSION"):
|
|
90
|
+
return ActorRef(
|
|
91
|
+
actor_type="agent",
|
|
92
|
+
actor_name="opencode",
|
|
93
|
+
tool="opencode",
|
|
94
|
+
session_id=session_id,
|
|
95
|
+
host=socket.gethostname(),
|
|
96
|
+
pid=os.getpid(),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if os.getenv("CODEX_VERSION"):
|
|
100
|
+
return ActorRef(
|
|
101
|
+
actor_type="agent",
|
|
102
|
+
actor_name="codex",
|
|
103
|
+
tool="codex",
|
|
104
|
+
session_id=session_id,
|
|
105
|
+
host=socket.gethostname(),
|
|
106
|
+
pid=os.getpid(),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if os.getenv("PI_VERSION"):
|
|
110
|
+
return ActorRef(
|
|
111
|
+
actor_type="agent",
|
|
112
|
+
actor_name="pi",
|
|
113
|
+
tool="pi",
|
|
114
|
+
session_id=session_id,
|
|
115
|
+
host=socket.gethostname(),
|
|
116
|
+
pid=os.getpid(),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if os.getenv("GITHUB_ACTIONS") == "true":
|
|
120
|
+
return ActorRef(
|
|
121
|
+
actor_type="system",
|
|
122
|
+
actor_name="github-actions",
|
|
123
|
+
tool="github-actions",
|
|
124
|
+
session_id=session_id or os.getenv("GITHUB_RUN_ID"),
|
|
125
|
+
host=socket.gethostname(),
|
|
126
|
+
pid=os.getpid(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if sys.stdin.isatty() and not any(
|
|
130
|
+
[
|
|
131
|
+
os.getenv("OPENCODE_VERSION"),
|
|
132
|
+
os.getenv("CODEX_VERSION"),
|
|
133
|
+
os.getenv("PI_VERSION"),
|
|
134
|
+
os.getenv("GITHUB_ACTIONS"),
|
|
135
|
+
]
|
|
136
|
+
):
|
|
137
|
+
return ActorRef(
|
|
138
|
+
actor_type="user",
|
|
139
|
+
actor_name=getpass.getuser() or "user",
|
|
140
|
+
session_id=session_id,
|
|
141
|
+
host=socket.gethostname(),
|
|
142
|
+
pid=os.getpid(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# 5. Safe default
|
|
146
|
+
return ActorRef(
|
|
147
|
+
actor_type="agent",
|
|
148
|
+
actor_name="taskledger",
|
|
149
|
+
tool=tool,
|
|
150
|
+
session_id=session_id,
|
|
151
|
+
host=socket.gethostname(),
|
|
152
|
+
pid=os.getpid(),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def resolve_harness(
|
|
157
|
+
*,
|
|
158
|
+
name: str | None = None,
|
|
159
|
+
kind: str | None = None,
|
|
160
|
+
session_id: str | None = None,
|
|
161
|
+
cwd: Path | None = None,
|
|
162
|
+
workspace_root: Path | None = None,
|
|
163
|
+
) -> HarnessRef:
|
|
164
|
+
"""
|
|
165
|
+
Resolve harness identity with fallback order:
|
|
166
|
+
1. Explicit parameters
|
|
167
|
+
2. Environment variables
|
|
168
|
+
3. Stored state (harness.yaml)
|
|
169
|
+
4. Auto-detect from environment
|
|
170
|
+
5. Safe default
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
# Use explicit params if provided
|
|
174
|
+
if name:
|
|
175
|
+
harness_id = next_project_id("harness", [])
|
|
176
|
+
return HarnessRef(
|
|
177
|
+
harness_id=harness_id,
|
|
178
|
+
name=name,
|
|
179
|
+
kind=normalize_harness_kind(kind or "unknown"),
|
|
180
|
+
session_id=session_id,
|
|
181
|
+
working_directory=str(cwd) if cwd else None,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Check environment
|
|
185
|
+
env_harness = os.getenv("TASKLEDGER_HARNESS")
|
|
186
|
+
if env_harness:
|
|
187
|
+
harness_id = next_project_id("harness", [])
|
|
188
|
+
return HarnessRef(
|
|
189
|
+
harness_id=harness_id,
|
|
190
|
+
name=env_harness,
|
|
191
|
+
kind=normalize_harness_kind(kind or "unknown"),
|
|
192
|
+
session_id=session_id or os.getenv("TASKLEDGER_SESSION_ID"),
|
|
193
|
+
working_directory=str(cwd) if cwd else None,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Check stored state
|
|
197
|
+
if workspace_root is not None:
|
|
198
|
+
stored = load_harness_state(workspace_root)
|
|
199
|
+
if stored is not None:
|
|
200
|
+
harness_id = next_project_id("harness", [])
|
|
201
|
+
return HarnessRef(
|
|
202
|
+
harness_id=harness_id,
|
|
203
|
+
name=stored.name,
|
|
204
|
+
kind=stored.kind,
|
|
205
|
+
session_id=stored.session_id or session_id,
|
|
206
|
+
working_directory=str(cwd) if cwd else None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Auto-detect
|
|
210
|
+
if os.getenv("OPENCODE_VERSION"):
|
|
211
|
+
harness_id = next_project_id("harness", [])
|
|
212
|
+
return HarnessRef(
|
|
213
|
+
harness_id=harness_id,
|
|
214
|
+
name="opencode",
|
|
215
|
+
kind="agent_harness",
|
|
216
|
+
session_id=session_id,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if os.getenv("CODEX_VERSION"):
|
|
220
|
+
harness_id = next_project_id("harness", [])
|
|
221
|
+
return HarnessRef(
|
|
222
|
+
harness_id=harness_id,
|
|
223
|
+
name="codex",
|
|
224
|
+
kind="agent_harness",
|
|
225
|
+
session_id=session_id,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if os.getenv("GITHUB_ACTIONS") == "true":
|
|
229
|
+
harness_id = next_project_id("harness", [])
|
|
230
|
+
return HarnessRef(
|
|
231
|
+
harness_id=harness_id,
|
|
232
|
+
name="github-actions",
|
|
233
|
+
kind="ci",
|
|
234
|
+
session_id=session_id or os.getenv("GITHUB_RUN_ID"),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Default
|
|
238
|
+
harness_id = next_project_id("harness", [])
|
|
239
|
+
return HarnessRef(
|
|
240
|
+
harness_id=harness_id,
|
|
241
|
+
name="unknown",
|
|
242
|
+
kind="unknown",
|
|
243
|
+
session_id=session_id,
|
|
244
|
+
working_directory=str(cwd) if cwd else None,
|
|
245
|
+
)
|