codealmanac 0.1.0.dev0__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.
- codealmanac/__init__.py +13 -0
- codealmanac/app.py +175 -0
- codealmanac/cli/__init__.py +1 -0
- codealmanac/cli/dispatch/__init__.py +0 -0
- codealmanac/cli/dispatch/admin.py +124 -0
- codealmanac/cli/dispatch/config.py +50 -0
- codealmanac/cli/dispatch/root.py +328 -0
- codealmanac/cli/main.py +28 -0
- codealmanac/cli/parser/__init__.py +0 -0
- codealmanac/cli/parser/admin.py +81 -0
- codealmanac/cli/parser/lifecycle.py +57 -0
- codealmanac/cli/parser/root.py +19 -0
- codealmanac/cli/parser/wiki.py +87 -0
- codealmanac/cli/render/__init__.py +0 -0
- codealmanac/cli/render/admin.py +191 -0
- codealmanac/cli/render/root.py +290 -0
- codealmanac/core/__init__.py +1 -0
- codealmanac/core/errors.py +45 -0
- codealmanac/core/models.py +14 -0
- codealmanac/core/paths.py +25 -0
- codealmanac/core/slug.py +7 -0
- codealmanac/core/text.py +5 -0
- codealmanac/database/__init__.py +15 -0
- codealmanac/database/sqlite.py +54 -0
- codealmanac/integrations/__init__.py +1 -0
- codealmanac/integrations/automation/__init__.py +3 -0
- codealmanac/integrations/automation/scheduler/__init__.py +5 -0
- codealmanac/integrations/automation/scheduler/launchd.py +163 -0
- codealmanac/integrations/command.py +56 -0
- codealmanac/integrations/harnesses/__init__.py +7 -0
- codealmanac/integrations/harnesses/claude/__init__.py +1 -0
- codealmanac/integrations/harnesses/claude/adapter.py +217 -0
- codealmanac/integrations/harnesses/codex/__init__.py +3 -0
- codealmanac/integrations/harnesses/codex/adapter.py +221 -0
- codealmanac/integrations/harnesses/git_status.py +49 -0
- codealmanac/integrations/sources/__init__.py +29 -0
- codealmanac/integrations/sources/filesystem/__init__.py +5 -0
- codealmanac/integrations/sources/filesystem/adapter.py +685 -0
- codealmanac/integrations/sources/filesystem/selection.py +209 -0
- codealmanac/integrations/sources/git/__init__.py +3 -0
- codealmanac/integrations/sources/git/adapter.py +132 -0
- codealmanac/integrations/sources/github/__init__.py +3 -0
- codealmanac/integrations/sources/github/adapter.py +413 -0
- codealmanac/integrations/sources/runtime.py +22 -0
- codealmanac/integrations/sources/transcripts/__init__.py +33 -0
- codealmanac/integrations/sources/transcripts/claude.py +61 -0
- codealmanac/integrations/sources/transcripts/codex.py +69 -0
- codealmanac/integrations/sources/transcripts/jsonl.py +84 -0
- codealmanac/integrations/sources/transcripts/runtime.py +387 -0
- codealmanac/integrations/sources/web/__init__.py +3 -0
- codealmanac/integrations/sources/web/adapter.py +303 -0
- codealmanac/integrations/updates/__init__.py +7 -0
- codealmanac/integrations/updates/package.py +85 -0
- codealmanac/integrations/workspaces/__init__.py +1 -0
- codealmanac/integrations/workspaces/git/__init__.py +3 -0
- codealmanac/integrations/workspaces/git/probe.py +128 -0
- codealmanac/manual/README.md +24 -0
- codealmanac/manual/__init__.py +19 -0
- codealmanac/manual/build.md +20 -0
- codealmanac/manual/evidence.md +23 -0
- codealmanac/manual/garden.md +20 -0
- codealmanac/manual/ingest.md +17 -0
- codealmanac/manual/library.py +84 -0
- codealmanac/manual/models.py +83 -0
- codealmanac/manual/pages.md +28 -0
- codealmanac/manual/requests.py +6 -0
- codealmanac/manual/sources.md +18 -0
- codealmanac/manual/style.md +19 -0
- codealmanac/prompts/__init__.py +5 -0
- codealmanac/prompts/base/notability.md +14 -0
- codealmanac/prompts/base/purpose.md +23 -0
- codealmanac/prompts/base/syntax.md +19 -0
- codealmanac/prompts/models.py +9 -0
- codealmanac/prompts/operations/garden.md +26 -0
- codealmanac/prompts/operations/ingest.md +18 -0
- codealmanac/prompts/renderer.py +24 -0
- codealmanac/prompts/requests.py +22 -0
- codealmanac/server/__init__.py +1 -0
- codealmanac/server/app.py +202 -0
- codealmanac/server/assets/__init__.py +1 -0
- codealmanac/server/assets/app.css +865 -0
- codealmanac/server/assets/app.js +3 -0
- codealmanac/server/assets/index.html +80 -0
- codealmanac/server/assets/viewer/api.js +30 -0
- codealmanac/server/assets/viewer/components.js +197 -0
- codealmanac/server/assets/viewer/main.js +126 -0
- codealmanac/server/assets/viewer/renderers.js +122 -0
- codealmanac/server/assets/viewer/routes.js +36 -0
- codealmanac/services/__init__.py +1 -0
- codealmanac/services/automation/__init__.py +3 -0
- codealmanac/services/automation/models.py +83 -0
- codealmanac/services/automation/ports.py +14 -0
- codealmanac/services/automation/requests.py +40 -0
- codealmanac/services/automation/service.py +294 -0
- codealmanac/services/config/__init__.py +17 -0
- codealmanac/services/config/models.py +61 -0
- codealmanac/services/config/requests.py +21 -0
- codealmanac/services/config/service.py +55 -0
- codealmanac/services/config/store.py +26 -0
- codealmanac/services/diagnostics/__init__.py +1 -0
- codealmanac/services/diagnostics/models.py +22 -0
- codealmanac/services/diagnostics/requests.py +8 -0
- codealmanac/services/diagnostics/service.py +283 -0
- codealmanac/services/harnesses/__init__.py +1 -0
- codealmanac/services/harnesses/models.py +104 -0
- codealmanac/services/harnesses/ports.py +18 -0
- codealmanac/services/harnesses/requests.py +19 -0
- codealmanac/services/harnesses/service.py +38 -0
- codealmanac/services/health/__init__.py +1 -0
- codealmanac/services/health/requests.py +8 -0
- codealmanac/services/health/service.py +20 -0
- codealmanac/services/index/__init__.py +1 -0
- codealmanac/services/index/models.py +135 -0
- codealmanac/services/index/requests.py +26 -0
- codealmanac/services/index/service.py +86 -0
- codealmanac/services/index/store.py +411 -0
- codealmanac/services/index/views.py +524 -0
- codealmanac/services/pages/__init__.py +1 -0
- codealmanac/services/pages/requests.py +17 -0
- codealmanac/services/pages/service.py +26 -0
- codealmanac/services/runs/__init__.py +1 -0
- codealmanac/services/runs/models.py +91 -0
- codealmanac/services/runs/requests.py +76 -0
- codealmanac/services/runs/service.py +86 -0
- codealmanac/services/runs/store.py +256 -0
- codealmanac/services/search/__init__.py +1 -0
- codealmanac/services/search/requests.py +23 -0
- codealmanac/services/search/service.py +31 -0
- codealmanac/services/sources/__init__.py +1 -0
- codealmanac/services/sources/models.py +126 -0
- codealmanac/services/sources/ports.py +30 -0
- codealmanac/services/sources/requests.py +76 -0
- codealmanac/services/sources/service.py +351 -0
- codealmanac/services/tagging/__init__.py +1 -0
- codealmanac/services/tagging/models.py +9 -0
- codealmanac/services/tagging/requests.py +35 -0
- codealmanac/services/tagging/service.py +43 -0
- codealmanac/services/topics/__init__.py +1 -0
- codealmanac/services/topics/models.py +36 -0
- codealmanac/services/topics/requests.py +115 -0
- codealmanac/services/topics/service.py +297 -0
- codealmanac/services/updates/__init__.py +4 -0
- codealmanac/services/updates/models.py +83 -0
- codealmanac/services/updates/ports.py +17 -0
- codealmanac/services/updates/requests.py +10 -0
- codealmanac/services/updates/service.py +113 -0
- codealmanac/services/viewer/__init__.py +1 -0
- codealmanac/services/viewer/models.py +80 -0
- codealmanac/services/viewer/renderer.py +89 -0
- codealmanac/services/viewer/requests.py +86 -0
- codealmanac/services/viewer/service.py +211 -0
- codealmanac/services/wiki/__init__.py +1 -0
- codealmanac/services/wiki/documents.py +83 -0
- codealmanac/services/wiki/frontmatter.py +94 -0
- codealmanac/services/wiki/frontmatter_rewrite.py +142 -0
- codealmanac/services/wiki/models.py +69 -0
- codealmanac/services/wiki/paths.py +42 -0
- codealmanac/services/wiki/service.py +57 -0
- codealmanac/services/wiki/templates.py +73 -0
- codealmanac/services/wiki/topics.py +266 -0
- codealmanac/services/wiki/wikilinks.py +58 -0
- codealmanac/services/workspaces/__init__.py +1 -0
- codealmanac/services/workspaces/models.py +124 -0
- codealmanac/services/workspaces/ports.py +9 -0
- codealmanac/services/workspaces/requests.py +82 -0
- codealmanac/services/workspaces/roots.py +74 -0
- codealmanac/services/workspaces/service.py +303 -0
- codealmanac/services/workspaces/store.py +127 -0
- codealmanac/workflows/__init__.py +1 -0
- codealmanac/workflows/build/__init__.py +1 -0
- codealmanac/workflows/build/models.py +8 -0
- codealmanac/workflows/build/service.py +45 -0
- codealmanac/workflows/garden/__init__.py +3 -0
- codealmanac/workflows/garden/models.py +30 -0
- codealmanac/workflows/garden/requests.py +22 -0
- codealmanac/workflows/garden/service.py +239 -0
- codealmanac/workflows/ingest/__init__.py +1 -0
- codealmanac/workflows/ingest/models.py +26 -0
- codealmanac/workflows/ingest/requests.py +39 -0
- codealmanac/workflows/ingest/service.py +302 -0
- codealmanac/workflows/lifecycle.py +197 -0
- codealmanac/workflows/sync/__init__.py +3 -0
- codealmanac/workflows/sync/models.py +157 -0
- codealmanac/workflows/sync/requests.py +63 -0
- codealmanac/workflows/sync/service.py +651 -0
- codealmanac/workflows/sync/store.py +51 -0
- codealmanac-0.1.0.dev0.dist-info/METADATA +248 -0
- codealmanac-0.1.0.dev0.dist-info/RECORD +192 -0
- codealmanac-0.1.0.dev0.dist-info/WHEEL +5 -0
- codealmanac-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- codealmanac-0.1.0.dev0.dist-info/licenses/LICENSE.md +201 -0
- codealmanac-0.1.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
from codealmanac.services.harnesses.models import HarnessTranscriptRef
|
|
7
|
+
from codealmanac.services.runs.models import RunEventKind, RunOperation, RunStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ListRunsRequest(CodeAlmanacModel):
|
|
11
|
+
cwd: Path
|
|
12
|
+
wiki: str | None = None
|
|
13
|
+
limit: int | None = None
|
|
14
|
+
|
|
15
|
+
@field_validator("limit")
|
|
16
|
+
@classmethod
|
|
17
|
+
def non_negative_limit(cls, value: int | None) -> int | None:
|
|
18
|
+
if value is not None and value < 0:
|
|
19
|
+
raise ValueError("limit must be non-negative")
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ShowRunRequest(CodeAlmanacModel):
|
|
24
|
+
cwd: Path
|
|
25
|
+
run_id: str
|
|
26
|
+
wiki: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReadRunLogRequest(CodeAlmanacModel):
|
|
30
|
+
cwd: Path
|
|
31
|
+
run_id: str
|
|
32
|
+
wiki: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StartRunRequest(CodeAlmanacModel):
|
|
36
|
+
cwd: Path
|
|
37
|
+
operation: RunOperation
|
|
38
|
+
wiki: str | None = None
|
|
39
|
+
title: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RecordRunEventRequest(CodeAlmanacModel):
|
|
43
|
+
cwd: Path
|
|
44
|
+
run_id: str
|
|
45
|
+
kind: RunEventKind
|
|
46
|
+
message: str
|
|
47
|
+
wiki: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MarkRunRunningRequest(CodeAlmanacModel):
|
|
51
|
+
cwd: Path
|
|
52
|
+
run_id: str
|
|
53
|
+
wiki: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RecordRunHarnessTranscriptRequest(CodeAlmanacModel):
|
|
57
|
+
cwd: Path
|
|
58
|
+
run_id: str
|
|
59
|
+
transcript: HarnessTranscriptRef
|
|
60
|
+
wiki: str | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FinishRunRequest(CodeAlmanacModel):
|
|
64
|
+
cwd: Path
|
|
65
|
+
run_id: str
|
|
66
|
+
status: RunStatus
|
|
67
|
+
wiki: str | None = None
|
|
68
|
+
summary: str | None = None
|
|
69
|
+
error: str | None = None
|
|
70
|
+
|
|
71
|
+
@field_validator("status")
|
|
72
|
+
@classmethod
|
|
73
|
+
def terminal_status(cls, value: RunStatus) -> RunStatus:
|
|
74
|
+
if value not in {RunStatus.DONE, RunStatus.FAILED, RunStatus.CANCELLED}:
|
|
75
|
+
raise ValueError("finish status must be done, failed, or cancelled")
|
|
76
|
+
return value
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from codealmanac.services.runs.models import RunLogEvent, RunRecord
|
|
4
|
+
from codealmanac.services.runs.requests import (
|
|
5
|
+
FinishRunRequest,
|
|
6
|
+
ListRunsRequest,
|
|
7
|
+
MarkRunRunningRequest,
|
|
8
|
+
ReadRunLogRequest,
|
|
9
|
+
RecordRunEventRequest,
|
|
10
|
+
RecordRunHarnessTranscriptRequest,
|
|
11
|
+
ShowRunRequest,
|
|
12
|
+
StartRunRequest,
|
|
13
|
+
)
|
|
14
|
+
from codealmanac.services.runs.store import RunStore
|
|
15
|
+
from codealmanac.services.workspaces.models import Workspace
|
|
16
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
17
|
+
from codealmanac.services.workspaces.service import WorkspacesService
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunsService:
|
|
21
|
+
def __init__(self, workspaces: WorkspacesService, store: RunStore):
|
|
22
|
+
self.workspaces = workspaces
|
|
23
|
+
self.store = store
|
|
24
|
+
|
|
25
|
+
def start(self, request: StartRunRequest) -> RunRecord:
|
|
26
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
27
|
+
return self.store.create(
|
|
28
|
+
workspace.almanac_path,
|
|
29
|
+
workspace.almanac_root,
|
|
30
|
+
workspace.workspace_id,
|
|
31
|
+
request.operation,
|
|
32
|
+
request.title,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def list(self, request: ListRunsRequest) -> tuple[RunRecord, ...]:
|
|
36
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
37
|
+
return self.store.list(workspace.almanac_path, request.limit)
|
|
38
|
+
|
|
39
|
+
def show(self, request: ShowRunRequest) -> RunRecord:
|
|
40
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
41
|
+
return self.store.read(workspace.almanac_path, request.run_id)
|
|
42
|
+
|
|
43
|
+
def log(self, request: ReadRunLogRequest) -> tuple[RunLogEvent, ...]:
|
|
44
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
45
|
+
return self.store.log(workspace.almanac_path, request.run_id)
|
|
46
|
+
|
|
47
|
+
def record_event(self, request: RecordRunEventRequest) -> RunLogEvent:
|
|
48
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
49
|
+
return self.store.append(
|
|
50
|
+
workspace.almanac_path,
|
|
51
|
+
request.run_id,
|
|
52
|
+
request.kind,
|
|
53
|
+
request.message,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def mark_running(self, request: MarkRunRunningRequest) -> RunRecord:
|
|
57
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
58
|
+
return self.store.mark_running(workspace.almanac_path, request.run_id)
|
|
59
|
+
|
|
60
|
+
def record_harness_transcript(
|
|
61
|
+
self,
|
|
62
|
+
request: RecordRunHarnessTranscriptRequest,
|
|
63
|
+
) -> RunRecord:
|
|
64
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
65
|
+
return self.store.record_harness_transcript(
|
|
66
|
+
workspace.almanac_path,
|
|
67
|
+
request.run_id,
|
|
68
|
+
request.transcript,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def finish(self, request: FinishRunRequest) -> RunRecord:
|
|
72
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
73
|
+
return self.store.finish(
|
|
74
|
+
workspace.almanac_path,
|
|
75
|
+
request.run_id,
|
|
76
|
+
request.status,
|
|
77
|
+
request.summary,
|
|
78
|
+
request.error,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def resolve_workspace(self, cwd: Path, wiki: str | None) -> Workspace:
|
|
82
|
+
if wiki is None:
|
|
83
|
+
return self.workspaces.resolve(cwd)
|
|
84
|
+
return self.workspaces.select(
|
|
85
|
+
SelectWorkspaceRequest(selector=wiki, base_path=cwd)
|
|
86
|
+
)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from datetime import UTC, datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from codealmanac.core.errors import ConflictError, NotFoundError
|
|
8
|
+
from codealmanac.services.harnesses.models import HarnessTranscriptRef
|
|
9
|
+
from codealmanac.services.runs.models import (
|
|
10
|
+
RunEventKind,
|
|
11
|
+
RunLogEvent,
|
|
12
|
+
RunOperation,
|
|
13
|
+
RunRecord,
|
|
14
|
+
RunStatus,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RunStore:
|
|
19
|
+
def create(
|
|
20
|
+
self,
|
|
21
|
+
almanac_path: Path,
|
|
22
|
+
almanac_root: Path,
|
|
23
|
+
workspace_id: str,
|
|
24
|
+
operation: RunOperation,
|
|
25
|
+
title: str | None,
|
|
26
|
+
) -> RunRecord:
|
|
27
|
+
now = datetime.now(UTC)
|
|
28
|
+
run_id = new_run_id(operation, now)
|
|
29
|
+
record = RunRecord(
|
|
30
|
+
run_id=run_id,
|
|
31
|
+
workspace_id=workspace_id,
|
|
32
|
+
operation=operation,
|
|
33
|
+
status=RunStatus.QUEUED,
|
|
34
|
+
title=title,
|
|
35
|
+
created_at=now,
|
|
36
|
+
updated_at=now,
|
|
37
|
+
log_path=run_log_reference_path(almanac_root, run_id),
|
|
38
|
+
)
|
|
39
|
+
write_record(almanac_path, record)
|
|
40
|
+
append_event(
|
|
41
|
+
almanac_path,
|
|
42
|
+
RunLogEvent(
|
|
43
|
+
run_id=run_id,
|
|
44
|
+
sequence=1,
|
|
45
|
+
timestamp=now,
|
|
46
|
+
kind=RunEventKind.STATUS,
|
|
47
|
+
message=f"queued {operation.value}",
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
return record
|
|
51
|
+
|
|
52
|
+
def list(self, almanac_path: Path, limit: int | None) -> tuple[RunRecord, ...]:
|
|
53
|
+
records = sorted(
|
|
54
|
+
iter_records(almanac_path),
|
|
55
|
+
key=lambda record: (record.created_at, record.run_id),
|
|
56
|
+
reverse=True,
|
|
57
|
+
)
|
|
58
|
+
if limit is not None:
|
|
59
|
+
return tuple(records[:limit])
|
|
60
|
+
return tuple(records)
|
|
61
|
+
|
|
62
|
+
def read(self, almanac_path: Path, run_id: str) -> RunRecord:
|
|
63
|
+
record = read_record(almanac_path, run_id)
|
|
64
|
+
if record is None:
|
|
65
|
+
raise NotFoundError("run", run_id)
|
|
66
|
+
return record
|
|
67
|
+
|
|
68
|
+
def log(self, almanac_path: Path, run_id: str) -> tuple[RunLogEvent, ...]:
|
|
69
|
+
self.read(almanac_path, run_id)
|
|
70
|
+
return tuple(iter_events(almanac_path, run_id))
|
|
71
|
+
|
|
72
|
+
def append(
|
|
73
|
+
self,
|
|
74
|
+
almanac_path: Path,
|
|
75
|
+
run_id: str,
|
|
76
|
+
kind: RunEventKind,
|
|
77
|
+
message: str,
|
|
78
|
+
) -> RunLogEvent:
|
|
79
|
+
record = self.read(almanac_path, run_id)
|
|
80
|
+
event = RunLogEvent(
|
|
81
|
+
run_id=run_id,
|
|
82
|
+
sequence=next_sequence(almanac_path, run_id),
|
|
83
|
+
timestamp=datetime.now(UTC),
|
|
84
|
+
kind=kind,
|
|
85
|
+
message=message,
|
|
86
|
+
)
|
|
87
|
+
append_event(almanac_path, event)
|
|
88
|
+
write_record(
|
|
89
|
+
almanac_path,
|
|
90
|
+
record.model_copy(update={"updated_at": event.timestamp}),
|
|
91
|
+
)
|
|
92
|
+
return event
|
|
93
|
+
|
|
94
|
+
def mark_running(self, almanac_path: Path, run_id: str) -> RunRecord:
|
|
95
|
+
record = self.read(almanac_path, run_id)
|
|
96
|
+
if record.status == RunStatus.RUNNING:
|
|
97
|
+
return record
|
|
98
|
+
if record.status != RunStatus.QUEUED:
|
|
99
|
+
raise ConflictError(
|
|
100
|
+
f"run {run_id} cannot start from {record.status.value}"
|
|
101
|
+
)
|
|
102
|
+
now = datetime.now(UTC)
|
|
103
|
+
running = record.model_copy(
|
|
104
|
+
update={
|
|
105
|
+
"status": RunStatus.RUNNING,
|
|
106
|
+
"updated_at": now,
|
|
107
|
+
"started_at": now,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
write_record(almanac_path, running)
|
|
111
|
+
append_event(
|
|
112
|
+
almanac_path,
|
|
113
|
+
RunLogEvent(
|
|
114
|
+
run_id=run_id,
|
|
115
|
+
sequence=next_sequence(almanac_path, run_id),
|
|
116
|
+
timestamp=now,
|
|
117
|
+
kind=RunEventKind.STATUS,
|
|
118
|
+
message=RunStatus.RUNNING.value,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
return running
|
|
122
|
+
|
|
123
|
+
def record_harness_transcript(
|
|
124
|
+
self,
|
|
125
|
+
almanac_path: Path,
|
|
126
|
+
run_id: str,
|
|
127
|
+
transcript: HarnessTranscriptRef,
|
|
128
|
+
) -> RunRecord:
|
|
129
|
+
record = self.read(almanac_path, run_id)
|
|
130
|
+
updated = record.model_copy(
|
|
131
|
+
update={
|
|
132
|
+
"harness_transcript": transcript,
|
|
133
|
+
"updated_at": datetime.now(UTC),
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
write_record(almanac_path, updated)
|
|
137
|
+
return updated
|
|
138
|
+
|
|
139
|
+
def finish(
|
|
140
|
+
self,
|
|
141
|
+
almanac_path: Path,
|
|
142
|
+
run_id: str,
|
|
143
|
+
status: RunStatus,
|
|
144
|
+
summary: str | None,
|
|
145
|
+
error: str | None,
|
|
146
|
+
) -> RunRecord:
|
|
147
|
+
record = self.read(almanac_path, run_id)
|
|
148
|
+
now = datetime.now(UTC)
|
|
149
|
+
finished = record.model_copy(
|
|
150
|
+
update={
|
|
151
|
+
"status": status,
|
|
152
|
+
"summary": summary,
|
|
153
|
+
"error": error,
|
|
154
|
+
"updated_at": now,
|
|
155
|
+
"finished_at": now,
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
write_record(almanac_path, finished)
|
|
159
|
+
append_event(
|
|
160
|
+
almanac_path,
|
|
161
|
+
RunLogEvent(
|
|
162
|
+
run_id=run_id,
|
|
163
|
+
sequence=next_sequence(almanac_path, run_id),
|
|
164
|
+
timestamp=now,
|
|
165
|
+
kind=RunEventKind.STATUS,
|
|
166
|
+
message=status.value,
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return finished
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def new_run_id(operation: RunOperation, now: datetime) -> str:
|
|
173
|
+
stamp = now.strftime("%Y%m%d%H%M%S")
|
|
174
|
+
return f"{operation.value}-{stamp}-{uuid4().hex[:8]}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def runs_dir(almanac_path: Path) -> Path:
|
|
178
|
+
return almanac_path / "jobs"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def run_record_path(almanac_path: Path, run_id: str) -> Path:
|
|
182
|
+
return runs_dir(almanac_path) / f"{run_id}.json"
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_log_path(almanac_path: Path, run_id: str) -> Path:
|
|
186
|
+
return runs_dir(almanac_path) / f"{run_id}.jsonl"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_log_reference_path(almanac_root: Path, run_id: str) -> Path:
|
|
190
|
+
return almanac_root / "jobs" / f"{run_id}.jsonl"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def write_record(almanac_path: Path, record: RunRecord) -> None:
|
|
194
|
+
path = run_record_path(almanac_path, record.run_id)
|
|
195
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
temporary = path.with_name(f".{path.name}.{uuid4().hex}.tmp")
|
|
197
|
+
try:
|
|
198
|
+
temporary.write_text(record.model_dump_json(indent=2), encoding="utf-8")
|
|
199
|
+
temporary.replace(path)
|
|
200
|
+
finally:
|
|
201
|
+
if temporary.exists():
|
|
202
|
+
temporary.unlink()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def read_record(almanac_path: Path, run_id: str) -> RunRecord | None:
|
|
206
|
+
path = run_record_path(almanac_path, run_id)
|
|
207
|
+
if not path.is_file():
|
|
208
|
+
return None
|
|
209
|
+
try:
|
|
210
|
+
return RunRecord.model_validate_json(path.read_text(encoding="utf-8"))
|
|
211
|
+
except (OSError, ValidationError, ValueError):
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def iter_records(almanac_path: Path) -> tuple[RunRecord, ...]:
|
|
216
|
+
directory = runs_dir(almanac_path)
|
|
217
|
+
if not directory.is_dir():
|
|
218
|
+
return ()
|
|
219
|
+
records: list[RunRecord] = []
|
|
220
|
+
for path in sorted(directory.glob("*.json")):
|
|
221
|
+
run_id = path.stem
|
|
222
|
+
record = read_record(almanac_path, run_id)
|
|
223
|
+
if record is not None:
|
|
224
|
+
records.append(record)
|
|
225
|
+
return tuple(records)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def append_event(almanac_path: Path, event: RunLogEvent) -> None:
|
|
229
|
+
path = run_log_path(almanac_path, event.run_id)
|
|
230
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
with path.open("a", encoding="utf-8") as file:
|
|
232
|
+
file.write(event.model_dump_json())
|
|
233
|
+
file.write("\n")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def iter_events(almanac_path: Path, run_id: str) -> tuple[RunLogEvent, ...]:
|
|
237
|
+
path = run_log_path(almanac_path, run_id)
|
|
238
|
+
if not path.is_file():
|
|
239
|
+
return ()
|
|
240
|
+
try:
|
|
241
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
242
|
+
except OSError:
|
|
243
|
+
return ()
|
|
244
|
+
events: list[RunLogEvent] = []
|
|
245
|
+
for line in lines:
|
|
246
|
+
if not line.strip():
|
|
247
|
+
continue
|
|
248
|
+
try:
|
|
249
|
+
events.append(RunLogEvent.model_validate_json(line))
|
|
250
|
+
except (ValidationError, ValueError):
|
|
251
|
+
continue
|
|
252
|
+
return tuple(events)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def next_sequence(almanac_path: Path, run_id: str) -> int:
|
|
256
|
+
return len(iter_events(almanac_path, run_id)) + 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SearchPagesRequest(CodeAlmanacModel):
|
|
9
|
+
cwd: Path
|
|
10
|
+
wiki: str | None = None
|
|
11
|
+
query: str | None = None
|
|
12
|
+
topics: tuple[str, ...] = ()
|
|
13
|
+
mentions: str | None = None
|
|
14
|
+
include_archive: bool = False
|
|
15
|
+
archived: bool = False
|
|
16
|
+
limit: int | None = None
|
|
17
|
+
|
|
18
|
+
@field_validator("limit")
|
|
19
|
+
@classmethod
|
|
20
|
+
def non_negative_limit(cls, value: int | None) -> int | None:
|
|
21
|
+
if value is not None and value < 0:
|
|
22
|
+
raise ValueError("limit must be non-negative")
|
|
23
|
+
return value
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from codealmanac.services.index.models import SearchPageResult
|
|
2
|
+
from codealmanac.services.index.requests import SearchIndexRequest
|
|
3
|
+
from codealmanac.services.index.service import IndexService
|
|
4
|
+
from codealmanac.services.search.requests import SearchPagesRequest
|
|
5
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
6
|
+
from codealmanac.services.workspaces.service import WorkspacesService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SearchService:
|
|
10
|
+
def __init__(self, workspaces: WorkspacesService, index: IndexService):
|
|
11
|
+
self.workspaces = workspaces
|
|
12
|
+
self.index = index
|
|
13
|
+
|
|
14
|
+
def search(self, request: SearchPagesRequest) -> tuple[SearchPageResult, ...]:
|
|
15
|
+
if request.wiki is None:
|
|
16
|
+
workspace = self.workspaces.resolve(request.cwd)
|
|
17
|
+
else:
|
|
18
|
+
workspace = self.workspaces.select(
|
|
19
|
+
SelectWorkspaceRequest(selector=request.wiki, base_path=request.cwd)
|
|
20
|
+
)
|
|
21
|
+
return self.index.search(
|
|
22
|
+
workspace.workspace_id,
|
|
23
|
+
SearchIndexRequest(
|
|
24
|
+
query=request.query,
|
|
25
|
+
topics=request.topics,
|
|
26
|
+
mentions=request.mentions,
|
|
27
|
+
include_archive=request.include_archive,
|
|
28
|
+
archived=request.archived,
|
|
29
|
+
limit=request.limit,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Source input contracts for lifecycle workflows."""
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import field_validator
|
|
6
|
+
|
|
7
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
8
|
+
from codealmanac.core.text import required_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SourceKind(StrEnum):
|
|
12
|
+
PATH_FILE = "path.file"
|
|
13
|
+
PATH_DIRECTORY = "path.directory"
|
|
14
|
+
PATH_UNKNOWN = "path.unknown"
|
|
15
|
+
GITHUB_PULL_REQUEST = "github.pull_request"
|
|
16
|
+
GITHUB_ISSUE = "github.issue"
|
|
17
|
+
WEB_URL = "web.url"
|
|
18
|
+
GIT_RANGE = "git.range"
|
|
19
|
+
GIT_DIFF = "git.diff"
|
|
20
|
+
TRANSCRIPT = "transcript"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SourceProvenanceKind(StrEnum):
|
|
24
|
+
FILE = "file"
|
|
25
|
+
DIRECTORY = "directory"
|
|
26
|
+
MISSING_PATH = "missing_path"
|
|
27
|
+
PR = "pr"
|
|
28
|
+
ISSUE = "issue"
|
|
29
|
+
URL = "url"
|
|
30
|
+
GIT = "git"
|
|
31
|
+
TRANSCRIPT = "transcript"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SourceRuntimeStatus(StrEnum):
|
|
35
|
+
AVAILABLE = "available"
|
|
36
|
+
SKIPPED = "skipped"
|
|
37
|
+
UNAVAILABLE = "unavailable"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TranscriptApp(StrEnum):
|
|
41
|
+
CLAUDE = "claude"
|
|
42
|
+
CODEX = "codex"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SourceAddress(CodeAlmanacModel):
|
|
46
|
+
raw: str
|
|
47
|
+
|
|
48
|
+
@field_validator("raw")
|
|
49
|
+
@classmethod
|
|
50
|
+
def require_raw(cls, value: str) -> str:
|
|
51
|
+
return required_text(value, "source address")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SourceRef(CodeAlmanacModel):
|
|
55
|
+
raw: str
|
|
56
|
+
kind: SourceKind
|
|
57
|
+
identity: str
|
|
58
|
+
path: Path | None = None
|
|
59
|
+
url: str | None = None
|
|
60
|
+
repository: str | None = None
|
|
61
|
+
number: int | None = None
|
|
62
|
+
revision_range: str | None = None
|
|
63
|
+
transcript: str | None = None
|
|
64
|
+
exists: bool | None = None
|
|
65
|
+
fingerprint: str | None = None
|
|
66
|
+
|
|
67
|
+
@field_validator("raw", "identity")
|
|
68
|
+
@classmethod
|
|
69
|
+
def require_text_fields(cls, value: str) -> str:
|
|
70
|
+
return required_text(value, "source reference")
|
|
71
|
+
|
|
72
|
+
@field_validator("number")
|
|
73
|
+
@classmethod
|
|
74
|
+
def positive_number(cls, value: int | None) -> int | None:
|
|
75
|
+
if value is not None and value < 1:
|
|
76
|
+
raise ValueError("source number must be positive")
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SourceBrief(CodeAlmanacModel):
|
|
81
|
+
ref: SourceRef
|
|
82
|
+
title: str
|
|
83
|
+
provenance_kind: SourceProvenanceKind
|
|
84
|
+
prompt_hint: str
|
|
85
|
+
|
|
86
|
+
@field_validator("title", "prompt_hint")
|
|
87
|
+
@classmethod
|
|
88
|
+
def require_brief_text(cls, value: str) -> str:
|
|
89
|
+
return required_text(value, "source brief")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class SourceRuntime(CodeAlmanacModel):
|
|
93
|
+
ref: SourceRef
|
|
94
|
+
status: SourceRuntimeStatus
|
|
95
|
+
title: str
|
|
96
|
+
content: str | None = None
|
|
97
|
+
diagnostics: tuple[str, ...] = ()
|
|
98
|
+
truncated: bool = False
|
|
99
|
+
|
|
100
|
+
@field_validator("title")
|
|
101
|
+
@classmethod
|
|
102
|
+
def require_title(cls, value: str) -> str:
|
|
103
|
+
return required_text(value, "source runtime title")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TranscriptCandidate(CodeAlmanacModel):
|
|
107
|
+
app: TranscriptApp
|
|
108
|
+
session_id: str
|
|
109
|
+
transcript_path: Path
|
|
110
|
+
cwd: Path
|
|
111
|
+
repo_root: Path
|
|
112
|
+
almanac_path: Path
|
|
113
|
+
modified_at: datetime
|
|
114
|
+
size_bytes: int
|
|
115
|
+
|
|
116
|
+
@field_validator("session_id")
|
|
117
|
+
@classmethod
|
|
118
|
+
def require_session_id(cls, value: str) -> str:
|
|
119
|
+
return required_text(value, "transcript session id")
|
|
120
|
+
|
|
121
|
+
@field_validator("size_bytes")
|
|
122
|
+
@classmethod
|
|
123
|
+
def non_negative_size(cls, value: int) -> int:
|
|
124
|
+
if value < 0:
|
|
125
|
+
raise ValueError("transcript size must be non-negative")
|
|
126
|
+
return value
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
from codealmanac.services.sources.models import (
|
|
4
|
+
SourceRef,
|
|
5
|
+
SourceRuntime,
|
|
6
|
+
TranscriptApp,
|
|
7
|
+
TranscriptCandidate,
|
|
8
|
+
)
|
|
9
|
+
from codealmanac.services.sources.requests import (
|
|
10
|
+
DiscoverTranscriptsRequest,
|
|
11
|
+
InspectSourceRuntimeRequest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TranscriptDiscoveryAdapter(Protocol):
|
|
16
|
+
app: TranscriptApp
|
|
17
|
+
|
|
18
|
+
def discover(
|
|
19
|
+
self,
|
|
20
|
+
request: DiscoverTranscriptsRequest,
|
|
21
|
+
) -> tuple[TranscriptCandidate, ...]:
|
|
22
|
+
"""Return local transcript candidates for one supported agent app."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SourceRuntimeAdapter(Protocol):
|
|
26
|
+
def supports(self, ref: SourceRef) -> bool:
|
|
27
|
+
"""Return true when this adapter can inspect the source ref."""
|
|
28
|
+
|
|
29
|
+
def inspect(self, request: InspectSourceRuntimeRequest) -> SourceRuntime:
|
|
30
|
+
"""Return bounded runtime material for one source ref."""
|