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,22 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
from codealmanac.core.text import required_text
|
|
7
|
+
from codealmanac.services.harnesses.models import HarnessKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunGardenRequest(CodeAlmanacModel):
|
|
11
|
+
cwd: Path
|
|
12
|
+
harness: HarnessKind
|
|
13
|
+
wiki: str | None = None
|
|
14
|
+
title: str | None = None
|
|
15
|
+
guidance: str | None = None
|
|
16
|
+
|
|
17
|
+
@field_validator("title", "guidance")
|
|
18
|
+
@classmethod
|
|
19
|
+
def require_optional_text(cls, value: str | None) -> str | None:
|
|
20
|
+
if value is None:
|
|
21
|
+
return None
|
|
22
|
+
return required_text(value, "garden request text")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codealmanac.prompts import PromptName, PromptRenderer, RenderPromptRequest
|
|
5
|
+
from codealmanac.services.harnesses.models import HarnessRunResult
|
|
6
|
+
from codealmanac.services.harnesses.requests import RunHarnessRequest
|
|
7
|
+
from codealmanac.services.harnesses.service import HarnessesService
|
|
8
|
+
from codealmanac.services.health.requests import HealthCheckRequest
|
|
9
|
+
from codealmanac.services.health.service import HealthService
|
|
10
|
+
from codealmanac.services.index.models import HealthReport, IndexSummary
|
|
11
|
+
from codealmanac.services.index.service import IndexService
|
|
12
|
+
from codealmanac.services.runs.models import RunEventKind, RunOperation, RunStatus
|
|
13
|
+
from codealmanac.services.runs.requests import (
|
|
14
|
+
FinishRunRequest,
|
|
15
|
+
MarkRunRunningRequest,
|
|
16
|
+
RecordRunEventRequest,
|
|
17
|
+
RecordRunHarnessTranscriptRequest,
|
|
18
|
+
StartRunRequest,
|
|
19
|
+
)
|
|
20
|
+
from codealmanac.services.runs.service import RunsService
|
|
21
|
+
from codealmanac.services.workspaces.models import Workspace
|
|
22
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
23
|
+
from codealmanac.services.workspaces.service import WorkspacesService
|
|
24
|
+
from codealmanac.workflows.garden.models import GardenPromptPayload, GardenResult
|
|
25
|
+
from codealmanac.workflows.garden.requests import RunGardenRequest
|
|
26
|
+
from codealmanac.workflows.lifecycle import (
|
|
27
|
+
LifecycleMutationPolicy,
|
|
28
|
+
first_line,
|
|
29
|
+
harness_events,
|
|
30
|
+
harness_run_event_kind,
|
|
31
|
+
validate_harness_result,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
GARDEN_PROMPT_SECTIONS = (
|
|
35
|
+
PromptName.BASE_PURPOSE,
|
|
36
|
+
PromptName.BASE_NOTABILITY,
|
|
37
|
+
PromptName.BASE_SYNTAX,
|
|
38
|
+
PromptName.OPERATION_GARDEN,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class GardenWorkflow:
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
workspaces: WorkspacesService,
|
|
46
|
+
harnesses: HarnessesService,
|
|
47
|
+
runs: RunsService,
|
|
48
|
+
index: IndexService,
|
|
49
|
+
health: HealthService,
|
|
50
|
+
mutation_policy: LifecycleMutationPolicy,
|
|
51
|
+
prompts: PromptRenderer,
|
|
52
|
+
):
|
|
53
|
+
self.workspaces = workspaces
|
|
54
|
+
self.harnesses = harnesses
|
|
55
|
+
self.runs = runs
|
|
56
|
+
self.index = index
|
|
57
|
+
self.health = health
|
|
58
|
+
self.mutation_policy = mutation_policy
|
|
59
|
+
self.prompts = prompts
|
|
60
|
+
|
|
61
|
+
def run(self, request: RunGardenRequest) -> GardenResult:
|
|
62
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
63
|
+
started = self.runs.start(
|
|
64
|
+
StartRunRequest(
|
|
65
|
+
cwd=request.cwd,
|
|
66
|
+
wiki=request.wiki,
|
|
67
|
+
operation=RunOperation.GARDEN,
|
|
68
|
+
title=request.title or "Garden wiki",
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
self.runs.mark_running(
|
|
72
|
+
MarkRunRunningRequest(
|
|
73
|
+
cwd=request.cwd,
|
|
74
|
+
wiki=request.wiki,
|
|
75
|
+
run_id=started.run_id,
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
index_before = self.index.summary(workspace.workspace_id)
|
|
80
|
+
health_before = self.health.check(
|
|
81
|
+
HealthCheckRequest(cwd=request.cwd, wiki=request.wiki)
|
|
82
|
+
)
|
|
83
|
+
self.record(
|
|
84
|
+
request,
|
|
85
|
+
started.run_id,
|
|
86
|
+
RunEventKind.MESSAGE,
|
|
87
|
+
"prepared garden context",
|
|
88
|
+
)
|
|
89
|
+
preflight = self.mutation_policy.preflight(workspace)
|
|
90
|
+
self.record(
|
|
91
|
+
request,
|
|
92
|
+
started.run_id,
|
|
93
|
+
RunEventKind.MESSAGE,
|
|
94
|
+
f"verified clean {workspace.almanac_root.as_posix()} preflight",
|
|
95
|
+
)
|
|
96
|
+
harness = self.harnesses.run(
|
|
97
|
+
RunHarnessRequest(
|
|
98
|
+
kind=request.harness,
|
|
99
|
+
cwd=workspace.root_path,
|
|
100
|
+
prompt=render_garden_prompt(
|
|
101
|
+
self.prompts,
|
|
102
|
+
workspace,
|
|
103
|
+
index_before,
|
|
104
|
+
health_before,
|
|
105
|
+
request.guidance,
|
|
106
|
+
),
|
|
107
|
+
title=request.title,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
self.record_harness_transcript(request, started.run_id, harness)
|
|
111
|
+
self.record_harness(request, started.run_id, harness)
|
|
112
|
+
safety = self.mutation_policy.validate(
|
|
113
|
+
preflight,
|
|
114
|
+
workspace,
|
|
115
|
+
harness.changed_files,
|
|
116
|
+
)
|
|
117
|
+
validate_harness_result(harness)
|
|
118
|
+
index_after = self.index.ensure_fresh(workspace.workspace_id)
|
|
119
|
+
finished = self.runs.finish(
|
|
120
|
+
FinishRunRequest(
|
|
121
|
+
cwd=request.cwd,
|
|
122
|
+
wiki=request.wiki,
|
|
123
|
+
run_id=started.run_id,
|
|
124
|
+
status=RunStatus.DONE,
|
|
125
|
+
summary=harness.summary or "garden completed",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return GardenResult(
|
|
129
|
+
run=finished,
|
|
130
|
+
harness=harness,
|
|
131
|
+
safety=safety,
|
|
132
|
+
index=index_after,
|
|
133
|
+
health_before=health_before,
|
|
134
|
+
)
|
|
135
|
+
except Exception as error:
|
|
136
|
+
self.fail_run(request, started.run_id, error)
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
def resolve_workspace(self, cwd: Path, wiki: str | None) -> Workspace:
|
|
140
|
+
if wiki is None:
|
|
141
|
+
return self.workspaces.resolve(cwd)
|
|
142
|
+
return self.workspaces.select(
|
|
143
|
+
SelectWorkspaceRequest(selector=wiki, base_path=cwd)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def record(
|
|
147
|
+
self,
|
|
148
|
+
request: RunGardenRequest,
|
|
149
|
+
run_id: str,
|
|
150
|
+
kind: RunEventKind,
|
|
151
|
+
message: str,
|
|
152
|
+
) -> None:
|
|
153
|
+
self.runs.record_event(
|
|
154
|
+
RecordRunEventRequest(
|
|
155
|
+
cwd=request.cwd,
|
|
156
|
+
wiki=request.wiki,
|
|
157
|
+
run_id=run_id,
|
|
158
|
+
kind=kind,
|
|
159
|
+
message=message,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def record_harness_transcript(
|
|
164
|
+
self,
|
|
165
|
+
request: RunGardenRequest,
|
|
166
|
+
run_id: str,
|
|
167
|
+
harness: HarnessRunResult,
|
|
168
|
+
) -> None:
|
|
169
|
+
if harness.transcript is None:
|
|
170
|
+
return
|
|
171
|
+
self.runs.record_harness_transcript(
|
|
172
|
+
RecordRunHarnessTranscriptRequest(
|
|
173
|
+
cwd=request.cwd,
|
|
174
|
+
wiki=request.wiki,
|
|
175
|
+
run_id=run_id,
|
|
176
|
+
transcript=harness.transcript,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def record_harness(
|
|
181
|
+
self,
|
|
182
|
+
request: RunGardenRequest,
|
|
183
|
+
run_id: str,
|
|
184
|
+
harness: HarnessRunResult,
|
|
185
|
+
) -> None:
|
|
186
|
+
for event in harness_events(harness):
|
|
187
|
+
self.record(
|
|
188
|
+
request,
|
|
189
|
+
run_id,
|
|
190
|
+
harness_run_event_kind(event),
|
|
191
|
+
event.message,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def fail_run(
|
|
195
|
+
self,
|
|
196
|
+
request: RunGardenRequest,
|
|
197
|
+
run_id: str,
|
|
198
|
+
error: Exception,
|
|
199
|
+
) -> None:
|
|
200
|
+
message = first_line(str(error)) or error.__class__.__name__
|
|
201
|
+
with suppress(Exception):
|
|
202
|
+
self.record(request, run_id, RunEventKind.ERROR, message)
|
|
203
|
+
self.runs.finish(
|
|
204
|
+
FinishRunRequest(
|
|
205
|
+
cwd=request.cwd,
|
|
206
|
+
wiki=request.wiki,
|
|
207
|
+
run_id=run_id,
|
|
208
|
+
status=RunStatus.FAILED,
|
|
209
|
+
error=message,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render_garden_prompt(
|
|
215
|
+
prompts: PromptRenderer,
|
|
216
|
+
workspace: Workspace,
|
|
217
|
+
index: IndexSummary,
|
|
218
|
+
health: HealthReport,
|
|
219
|
+
guidance: str | None,
|
|
220
|
+
) -> str:
|
|
221
|
+
payload = GardenPromptPayload(
|
|
222
|
+
workspace_name=workspace.name,
|
|
223
|
+
workspace_root=workspace.root_path,
|
|
224
|
+
almanac_root=workspace.almanac_path,
|
|
225
|
+
pages_root=workspace.almanac_path / "pages",
|
|
226
|
+
topics_file=workspace.almanac_path / "topics.yaml",
|
|
227
|
+
index=index,
|
|
228
|
+
health=health,
|
|
229
|
+
guidance=guidance,
|
|
230
|
+
)
|
|
231
|
+
return prompts.render(
|
|
232
|
+
RenderPromptRequest(
|
|
233
|
+
sections=GARDEN_PROMPT_SECTIONS,
|
|
234
|
+
context=(
|
|
235
|
+
"Runtime context:\n"
|
|
236
|
+
f"{payload.model_dump_json(indent=2)}\n",
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Ingest workflow orchestration."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
4
|
+
from codealmanac.services.harnesses.models import HarnessRunResult
|
|
5
|
+
from codealmanac.services.index.models import IndexRefreshResult
|
|
6
|
+
from codealmanac.services.runs.models import RunRecord
|
|
7
|
+
from codealmanac.services.sources.models import SourceBrief, SourceRuntime
|
|
8
|
+
from codealmanac.workflows.lifecycle import LifecycleMutationReport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IngestPromptPayload(CodeAlmanacModel):
|
|
12
|
+
workspace_name: str
|
|
13
|
+
workspace_root: Path
|
|
14
|
+
almanac_root: Path
|
|
15
|
+
sources: tuple[SourceBrief, ...]
|
|
16
|
+
source_runtime: tuple[SourceRuntime, ...]
|
|
17
|
+
guidance: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class IngestResult(CodeAlmanacModel):
|
|
21
|
+
run: RunRecord
|
|
22
|
+
sources: tuple[SourceBrief, ...]
|
|
23
|
+
source_runtime: tuple[SourceRuntime, ...]
|
|
24
|
+
harness: HarnessRunResult
|
|
25
|
+
safety: LifecycleMutationReport
|
|
26
|
+
index: IndexRefreshResult
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic import field_validator
|
|
4
|
+
|
|
5
|
+
from codealmanac.core.models import CodeAlmanacModel
|
|
6
|
+
from codealmanac.core.text import required_text
|
|
7
|
+
from codealmanac.services.harnesses.models import HarnessKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RunIngestRequest(CodeAlmanacModel):
|
|
11
|
+
cwd: Path
|
|
12
|
+
inputs: tuple[str, ...]
|
|
13
|
+
harness: HarnessKind
|
|
14
|
+
wiki: str | None = None
|
|
15
|
+
title: str | None = None
|
|
16
|
+
guidance: str | None = None
|
|
17
|
+
|
|
18
|
+
@field_validator("inputs")
|
|
19
|
+
@classmethod
|
|
20
|
+
def require_inputs(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
21
|
+
if len(value) == 0:
|
|
22
|
+
raise ValueError("at least one ingest input is required")
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
@field_validator("title", "guidance")
|
|
26
|
+
@classmethod
|
|
27
|
+
def require_optional_text(cls, value: str | None) -> str | None:
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
return required_text(value, "ingest request text")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RunIngestWithRunRequest(RunIngestRequest):
|
|
34
|
+
run_id: str
|
|
35
|
+
|
|
36
|
+
@field_validator("run_id")
|
|
37
|
+
@classmethod
|
|
38
|
+
def require_run_id(cls, value: str) -> str:
|
|
39
|
+
return required_text(value, "ingest run id")
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from codealmanac.prompts import PromptName, PromptRenderer, RenderPromptRequest
|
|
5
|
+
from codealmanac.services.harnesses.models import HarnessRunResult
|
|
6
|
+
from codealmanac.services.harnesses.requests import RunHarnessRequest
|
|
7
|
+
from codealmanac.services.harnesses.service import HarnessesService
|
|
8
|
+
from codealmanac.services.index.service import IndexService
|
|
9
|
+
from codealmanac.services.runs.models import (
|
|
10
|
+
RunEventKind,
|
|
11
|
+
RunOperation,
|
|
12
|
+
RunRecord,
|
|
13
|
+
RunStatus,
|
|
14
|
+
)
|
|
15
|
+
from codealmanac.services.runs.requests import (
|
|
16
|
+
FinishRunRequest,
|
|
17
|
+
MarkRunRunningRequest,
|
|
18
|
+
RecordRunEventRequest,
|
|
19
|
+
RecordRunHarnessTranscriptRequest,
|
|
20
|
+
StartRunRequest,
|
|
21
|
+
)
|
|
22
|
+
from codealmanac.services.runs.service import RunsService
|
|
23
|
+
from codealmanac.services.sources.models import SourceBrief, SourceRuntime
|
|
24
|
+
from codealmanac.services.sources.requests import (
|
|
25
|
+
InspectSourceRuntimeRequest,
|
|
26
|
+
ResolveSourcesRequest,
|
|
27
|
+
SourceRuntimeContext,
|
|
28
|
+
)
|
|
29
|
+
from codealmanac.services.sources.service import SourcesService
|
|
30
|
+
from codealmanac.services.workspaces.models import Workspace
|
|
31
|
+
from codealmanac.services.workspaces.requests import SelectWorkspaceRequest
|
|
32
|
+
from codealmanac.services.workspaces.service import WorkspacesService
|
|
33
|
+
from codealmanac.workflows.ingest.models import IngestPromptPayload, IngestResult
|
|
34
|
+
from codealmanac.workflows.ingest.requests import (
|
|
35
|
+
RunIngestRequest,
|
|
36
|
+
RunIngestWithRunRequest,
|
|
37
|
+
)
|
|
38
|
+
from codealmanac.workflows.lifecycle import (
|
|
39
|
+
LifecycleMutationPolicy,
|
|
40
|
+
first_line,
|
|
41
|
+
harness_events,
|
|
42
|
+
harness_run_event_kind,
|
|
43
|
+
validate_harness_result,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
INGEST_PROMPT_SECTIONS = (
|
|
47
|
+
PromptName.BASE_PURPOSE,
|
|
48
|
+
PromptName.BASE_NOTABILITY,
|
|
49
|
+
PromptName.BASE_SYNTAX,
|
|
50
|
+
PromptName.OPERATION_INGEST,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class IngestWorkflow:
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
workspaces: WorkspacesService,
|
|
58
|
+
sources: SourcesService,
|
|
59
|
+
harnesses: HarnessesService,
|
|
60
|
+
runs: RunsService,
|
|
61
|
+
index: IndexService,
|
|
62
|
+
mutation_policy: LifecycleMutationPolicy,
|
|
63
|
+
prompts: PromptRenderer,
|
|
64
|
+
):
|
|
65
|
+
self.workspaces = workspaces
|
|
66
|
+
self.sources = sources
|
|
67
|
+
self.harnesses = harnesses
|
|
68
|
+
self.runs = runs
|
|
69
|
+
self.index = index
|
|
70
|
+
self.mutation_policy = mutation_policy
|
|
71
|
+
self.prompts = prompts
|
|
72
|
+
|
|
73
|
+
def run(self, request: RunIngestRequest) -> IngestResult:
|
|
74
|
+
started = self.start(request)
|
|
75
|
+
return self.run_with_run(
|
|
76
|
+
RunIngestWithRunRequest(
|
|
77
|
+
cwd=request.cwd,
|
|
78
|
+
inputs=request.inputs,
|
|
79
|
+
harness=request.harness,
|
|
80
|
+
wiki=request.wiki,
|
|
81
|
+
title=request.title,
|
|
82
|
+
guidance=request.guidance,
|
|
83
|
+
run_id=started.run_id,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def start(self, request: RunIngestRequest) -> RunRecord:
|
|
88
|
+
return self.runs.start(
|
|
89
|
+
StartRunRequest(
|
|
90
|
+
cwd=request.cwd,
|
|
91
|
+
wiki=request.wiki,
|
|
92
|
+
operation=RunOperation.INGEST,
|
|
93
|
+
title=request.title or default_title(request.inputs),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def run_with_run(self, request: RunIngestWithRunRequest) -> IngestResult:
|
|
98
|
+
workspace = self.resolve_workspace(request.cwd, request.wiki)
|
|
99
|
+
run_id = request.run_id
|
|
100
|
+
try:
|
|
101
|
+
self.runs.mark_running(
|
|
102
|
+
MarkRunRunningRequest(
|
|
103
|
+
cwd=request.cwd,
|
|
104
|
+
wiki=request.wiki,
|
|
105
|
+
run_id=run_id,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
preflight = self.mutation_policy.preflight(workspace)
|
|
109
|
+
self.record(
|
|
110
|
+
request,
|
|
111
|
+
run_id,
|
|
112
|
+
RunEventKind.MESSAGE,
|
|
113
|
+
f"verified clean {workspace.almanac_root.as_posix()} preflight",
|
|
114
|
+
)
|
|
115
|
+
sources = self.sources.resolve(
|
|
116
|
+
ResolveSourcesRequest(cwd=request.cwd, inputs=request.inputs)
|
|
117
|
+
)
|
|
118
|
+
self.record(
|
|
119
|
+
request,
|
|
120
|
+
run_id,
|
|
121
|
+
RunEventKind.MESSAGE,
|
|
122
|
+
f"resolved {len(sources)} {source_word(len(sources))}",
|
|
123
|
+
)
|
|
124
|
+
source_runtime = self.inspect_source_runtime(workspace, sources)
|
|
125
|
+
self.record(
|
|
126
|
+
request,
|
|
127
|
+
run_id,
|
|
128
|
+
RunEventKind.MESSAGE,
|
|
129
|
+
f"loaded {len(source_runtime)} source runtime snapshot"
|
|
130
|
+
f"{'' if len(source_runtime) == 1 else 's'}",
|
|
131
|
+
)
|
|
132
|
+
harness = self.harnesses.run(
|
|
133
|
+
RunHarnessRequest(
|
|
134
|
+
kind=request.harness,
|
|
135
|
+
cwd=workspace.root_path,
|
|
136
|
+
prompt=render_ingest_prompt(
|
|
137
|
+
self.prompts,
|
|
138
|
+
workspace,
|
|
139
|
+
sources,
|
|
140
|
+
source_runtime,
|
|
141
|
+
request.guidance,
|
|
142
|
+
),
|
|
143
|
+
title=request.title,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
self.record_harness_transcript(request, run_id, harness)
|
|
147
|
+
self.record_harness_events(request, run_id, harness)
|
|
148
|
+
safety = self.mutation_policy.validate(
|
|
149
|
+
preflight,
|
|
150
|
+
workspace,
|
|
151
|
+
harness.changed_files,
|
|
152
|
+
)
|
|
153
|
+
validate_harness_result(harness)
|
|
154
|
+
index = self.index.ensure_fresh(workspace.workspace_id)
|
|
155
|
+
finished = self.runs.finish(
|
|
156
|
+
FinishRunRequest(
|
|
157
|
+
cwd=request.cwd,
|
|
158
|
+
wiki=request.wiki,
|
|
159
|
+
run_id=run_id,
|
|
160
|
+
status=RunStatus.DONE,
|
|
161
|
+
summary=harness.summary or "ingest completed",
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return IngestResult(
|
|
165
|
+
run=finished,
|
|
166
|
+
sources=sources,
|
|
167
|
+
source_runtime=source_runtime,
|
|
168
|
+
harness=harness,
|
|
169
|
+
safety=safety,
|
|
170
|
+
index=index,
|
|
171
|
+
)
|
|
172
|
+
except Exception as error:
|
|
173
|
+
self.fail_run(request, run_id, error)
|
|
174
|
+
raise
|
|
175
|
+
|
|
176
|
+
def resolve_workspace(self, cwd: Path, wiki: str | None) -> Workspace:
|
|
177
|
+
if wiki is None:
|
|
178
|
+
return self.workspaces.resolve(cwd)
|
|
179
|
+
return self.workspaces.select(
|
|
180
|
+
SelectWorkspaceRequest(selector=wiki, base_path=cwd)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def record(
|
|
184
|
+
self,
|
|
185
|
+
request: RunIngestRequest,
|
|
186
|
+
run_id: str,
|
|
187
|
+
kind: RunEventKind,
|
|
188
|
+
message: str,
|
|
189
|
+
) -> None:
|
|
190
|
+
self.runs.record_event(
|
|
191
|
+
RecordRunEventRequest(
|
|
192
|
+
cwd=request.cwd,
|
|
193
|
+
wiki=request.wiki,
|
|
194
|
+
run_id=run_id,
|
|
195
|
+
kind=kind,
|
|
196
|
+
message=message,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def record_harness_transcript(
|
|
201
|
+
self,
|
|
202
|
+
request: RunIngestRequest,
|
|
203
|
+
run_id: str,
|
|
204
|
+
harness: HarnessRunResult,
|
|
205
|
+
) -> None:
|
|
206
|
+
if harness.transcript is None:
|
|
207
|
+
return
|
|
208
|
+
self.runs.record_harness_transcript(
|
|
209
|
+
RecordRunHarnessTranscriptRequest(
|
|
210
|
+
cwd=request.cwd,
|
|
211
|
+
wiki=request.wiki,
|
|
212
|
+
run_id=run_id,
|
|
213
|
+
transcript=harness.transcript,
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def record_harness_events(
|
|
218
|
+
self,
|
|
219
|
+
request: RunIngestRequest,
|
|
220
|
+
run_id: str,
|
|
221
|
+
harness: HarnessRunResult,
|
|
222
|
+
) -> None:
|
|
223
|
+
for event in harness_events(harness):
|
|
224
|
+
self.record(
|
|
225
|
+
request,
|
|
226
|
+
run_id,
|
|
227
|
+
harness_run_event_kind(event),
|
|
228
|
+
event.message,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def inspect_source_runtime(
|
|
232
|
+
self,
|
|
233
|
+
workspace: Workspace,
|
|
234
|
+
sources: tuple[SourceBrief, ...],
|
|
235
|
+
) -> tuple[SourceRuntime, ...]:
|
|
236
|
+
return tuple(
|
|
237
|
+
self.sources.inspect_runtime(
|
|
238
|
+
InspectSourceRuntimeRequest(
|
|
239
|
+
cwd=workspace.root_path,
|
|
240
|
+
ref=source.ref,
|
|
241
|
+
context=SourceRuntimeContext(
|
|
242
|
+
ignored_directories=(workspace.almanac_root,)
|
|
243
|
+
),
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
for source in sources
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def fail_run(
|
|
250
|
+
self,
|
|
251
|
+
request: RunIngestRequest,
|
|
252
|
+
run_id: str,
|
|
253
|
+
error: Exception,
|
|
254
|
+
) -> None:
|
|
255
|
+
message = first_line(str(error)) or error.__class__.__name__
|
|
256
|
+
with suppress(Exception):
|
|
257
|
+
self.record(request, run_id, RunEventKind.ERROR, message)
|
|
258
|
+
self.runs.finish(
|
|
259
|
+
FinishRunRequest(
|
|
260
|
+
cwd=request.cwd,
|
|
261
|
+
wiki=request.wiki,
|
|
262
|
+
run_id=run_id,
|
|
263
|
+
status=RunStatus.FAILED,
|
|
264
|
+
error=message,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def render_ingest_prompt(
|
|
270
|
+
prompts: PromptRenderer,
|
|
271
|
+
workspace: Workspace,
|
|
272
|
+
sources: tuple[SourceBrief, ...],
|
|
273
|
+
source_runtime: tuple[SourceRuntime, ...],
|
|
274
|
+
guidance: str | None,
|
|
275
|
+
) -> str:
|
|
276
|
+
payload = IngestPromptPayload(
|
|
277
|
+
workspace_name=workspace.name,
|
|
278
|
+
workspace_root=workspace.root_path,
|
|
279
|
+
almanac_root=workspace.almanac_path,
|
|
280
|
+
sources=sources,
|
|
281
|
+
source_runtime=source_runtime,
|
|
282
|
+
guidance=guidance,
|
|
283
|
+
)
|
|
284
|
+
return prompts.render(
|
|
285
|
+
RenderPromptRequest(
|
|
286
|
+
sections=INGEST_PROMPT_SECTIONS,
|
|
287
|
+
context=(
|
|
288
|
+
"Runtime context:\n"
|
|
289
|
+
f"{payload.model_dump_json(indent=2)}\n",
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def default_title(inputs: tuple[str, ...]) -> str:
|
|
296
|
+
if len(inputs) == 1:
|
|
297
|
+
return f"Ingest {inputs[0]}"
|
|
298
|
+
return f"Ingest {len(inputs)} sources"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def source_word(count: int) -> str:
|
|
302
|
+
return "source" if count == 1 else "sources"
|