Skip to content

Commit 9b2c8bd

Browse files
committed
feat(launch): materialize source bundles
1 parent f580fc3 commit 9b2c8bd

28 files changed

Lines changed: 1082 additions & 80 deletions

docs/codealmanac-launch/next-agent-brief.md

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,22 @@ verification, launch-folder updates, commit, and push.
1010

1111
## Last Completed Slice
1212

13-
Slice 10 added branch-head staling.
13+
Slice 11 added source bundle materialization.
1414

1515
Implemented:
1616

17-
- `app.control.get_run(...)`
18-
- staling of queued/running runs when a newer trigger arrives for the same
19-
branch with a different expected head
20-
- normalized stale run events
21-
- preservation of terminal runs
22-
- preservation of queued/running runs already targeting the new head
23-
- removal of unused `EnsureControlSchemaRequest`
17+
- control DB verbs for sessions, turns, turn-branch links, and branch session
18+
selection
19+
- `app.source_bundles`
20+
- `sources/manifest.json`
21+
- copied full session files under `sources/sessions/<provider>/`
22+
- local run preparation that materializes source bundles before writing
23+
`request.json`
2424

2525
Verified:
2626

2727
```text
28-
uv run pytest tests/test_control_service.py tests/test_architecture.py
28+
uv run pytest tests/test_control_service.py tests/test_source_bundles_service.py tests/test_local_run_preparation_workflow.py tests/test_architecture.py
2929
uv run pytest
3030
uv run ruff check .
3131
git diff --check
@@ -38,7 +38,6 @@ Choose the next substantial slice from the launch plan. Good candidates:
3838
- local trigger event recording through Git hooks
3939
- public or hidden setup command that calls `app.local_hooks`
4040
- local run storage bridge from repo-local job files to the control DB
41-
- source bundle selection materialization for claimed runs
4241
- local model worker execution from prepared `EngineRunRequest`
4342
- delivery commit application from `EngineRunResult`
4443

@@ -48,7 +47,7 @@ progress update, commit, and push.
4847

4948
## Known Repo State
5049

51-
The branch is `dev`. At the start of Slice 10 it was clean and even with
50+
The branch is `dev`. At the start of Slice 11 it was clean and even with
5251
`origin/dev`.
5352

5453
The local wiki command currently fails on this checkout with:

docs/codealmanac-launch/progress.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Percentages are planning estimates, not accounting metrics.
88

99
## Latest RelayForge Update
1010

11-
Sent: 2026-07-02 after Slice 10 verification.
11+
Sent: 2026-07-02 after Slice 11 verification.
1212

1313
Route:
1414

@@ -26,7 +26,7 @@ It was not present in `codealmanac/prd` when checked.
2626

2727
| Area | Latest | Previous | Basis |
2828
| --- | ---: | ---: | --- |
29-
| CodeAlmanac backend/local | 50% | 47% | Slice 10 added branch-head staling so newer triggers mark older queued/running runs stale before delivery can use an old expected head. |
29+
| CodeAlmanac backend/local | 53% | 50% | Slice 11 added branch session selection and local source bundle materialization into worker `sources/`. |
3030
| CodeAlmanac CLI/public UX | 10% | 10% | Hidden trigger command exists; launch public CLI surface is not rebuilt yet. |
3131
| CodeAlmanac-hosted backend/auth/API | 8% | 8% | WorkOS/Doppler setup and hosted rename groundwork exist; public API migration is not implemented yet. |
3232
| Hosted frontend/onboarding | 5% | 5% | Existing frontend is deployed under CodeAlmanac; new onboarding/configuration screens are not implemented yet. |

docs/codealmanac-launch/schema-contract.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Local worker workspaces:
5555
~/.codealmanac/workspaces/<run-id>/
5656
repo/
5757
sources/
58+
manifest.json
59+
sessions/
5860
run/
5961
```
6062

docs/codealmanac-launch/verification-matrix.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,15 @@ Current evidence:
111111
runs are preserved.
112112
- `tests/test_control_service.py` proves stale runs receive normalized status
113113
run events.
114+
- Slice 11 added branch session selection and source bundle materialization.
115+
- `tests/test_control_service.py` proves branch selection returns distinct full
116+
sessions through `turn_branches`.
117+
- `tests/test_source_bundles_service.py` proves session source files are copied
118+
into `sources/sessions/<provider>/` and recorded in `manifest.json`.
119+
- `tests/test_local_run_preparation_workflow.py` proves local run preparation
120+
materializes the source bundle before writing the engine request.
121+
- `tests/test_architecture.py` proves source-bundle materialization remains a
122+
separate service boundary.
114123

115124
Commands:
116125

docs/codealmanac-launch/worklog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,21 @@
225225
`uv run pytest tests/test_control_service.py tests/test_architecture.py`.
226226
- Verified Slice 10 full gate with `uv run pytest` (`394 passed`),
227227
`uv run ruff check .`, and `git diff --check`.
228+
- Sent the Slice 10 RelayForge update and recorded progress as:
229+
CodeAlmanac backend/local 50%, CLI/public UX 10%,
230+
CodeAlmanac-hosted backend/auth/API 8%, hosted frontend/onboarding 5%, and
231+
infra/deploy rename 5%.
232+
- Planned Slice 11 in
233+
`docs/plans/2026-07-02-slice-11-source-bundle-materialization.md`.
234+
- Added control DB verbs for sessions, turns, turn-branch links, and distinct
235+
branch session selection.
236+
- Added `app.source_bundles` with a manifest-backed local source bundle
237+
materializer.
238+
- Wired local run preparation to materialize `sources/manifest.json` and copied
239+
session files before writing the engine request.
240+
- Added architecture guards for the source-bundle service boundary.
241+
- Corrected the launch plan commit-subject example to `docs almanac:`.
242+
- Verified Slice 11 focused behavior with
243+
`uv run pytest tests/test_control_service.py tests/test_source_bundles_service.py tests/test_local_run_preparation_workflow.py tests/test_architecture.py`.
244+
- Verified Slice 11 full gate with `uv run pytest` (`398 passed`),
245+
`uv run ruff check .`, and `git diff --check`.

docs/plans/2026-07-02-codealmanac-cloud-local-launch.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ nouns and execution shape, not a separate conceptual product.
4040
- Use commit subject format:
4141

4242
```text
43-
almanac: <summary>
43+
docs almanac: <summary>
4444
```
4545

4646
## Target Shape
@@ -108,7 +108,7 @@ EngineRunRequest
108108
expected_head_sha
109109
operation
110110
almanac_root
111-
commit_subject_style = "almanac: <summary>"
111+
commit_subject_style = "docs almanac: <summary>"
112112
113113
EngineRunResult
114114
status
@@ -141,7 +141,7 @@ Git hook fires
141141
-> publisher collects wiki patch
142142
-> publisher checks current branch still equals expected_head_sha
143143
-> publisher applies patch to real checkout
144-
-> publisher commits "almanac: <summary>"
144+
-> publisher commits "docs almanac: <summary>"
145145
```
146146

147147
Local workspace:
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Slice 11: Source Bundle Materialization
2+
3+
Date: 2026-07-02.
4+
Status: planned.
5+
6+
## Goal
7+
8+
Materialize the local worker `sources/` folder during local run preparation.
9+
The run request should continue to pass a `sources_path` and
10+
`source_bundle_ref`; it should not inline session contents or selected source
11+
payloads.
12+
13+
## Shape
14+
15+
```python
16+
sessions = control.list_sessions_for_branch(branch.id)
17+
bundle = source_bundles.materialize(
18+
run_id=run.id,
19+
branch_id=branch.id,
20+
target_path=worker_workspace.paths.sources_path,
21+
sessions=sessions,
22+
)
23+
engine = engine_runs.prepare(..., sources_path=bundle.root_path)
24+
```
25+
26+
The control DB owns session/turn/branch relationships. The source-bundle service
27+
owns file materialization and the manifest format. The local-run workflow only
28+
orchestrates services.
29+
30+
## In Scope
31+
32+
- Add control service verbs for:
33+
- upserting sessions
34+
- upserting turns
35+
- linking turns to branches
36+
- listing distinct sessions connected to a branch
37+
- Add `services/source_bundles` with:
38+
- typed manifest models
39+
- a materialization request
40+
- a store that copies referenced local session files into `sources/`
41+
- `manifest.json`
42+
- Wire local run preparation to materialize the source bundle before writing
43+
`request.json`.
44+
- Add tests for control session selection, source bundle materialization, and
45+
local run preparation.
46+
- Update launch docs and progress.
47+
48+
## Out Of Scope
49+
50+
- Conversation capture hooks.
51+
- Branch detection algorithms.
52+
- Git commit range evidence.
53+
- Model execution.
54+
- Delivery application.
55+
56+
## Design Decisions
57+
58+
- The selector is intentionally simple: include every full session with at least
59+
one turn linked to the branch.
60+
- The bundle copies source files into the worker workspace, but the run request
61+
still passes the bundle by reference via `sources_path` and
62+
`source_bundle_ref`.
63+
- Session destination filenames use the stable local session row id, not raw
64+
provider paths.
65+
- Missing or unsupported session refs fail preparation. That is better than
66+
silently running with incomplete source context.
67+
68+
## Verification
69+
70+
Focused:
71+
72+
```text
73+
uv run pytest tests/test_control_service.py tests/test_source_bundles_service.py tests/test_local_run_preparation_workflow.py tests/test_architecture.py
74+
```
75+
76+
Full:
77+
78+
```text
79+
uv run pytest
80+
uv run ruff check .
81+
git diff --check
82+
```

src/codealmanac/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from codealmanac.services.search.service import SearchService
4848
from codealmanac.services.setup.ports import InstructionInstaller
4949
from codealmanac.services.setup.service import SetupService
50+
from codealmanac.services.source_bundles.service import SourceBundlesService
51+
from codealmanac.services.source_bundles.store import SourceBundlesStore
5052
from codealmanac.services.sources.ports import (
5153
SourceRuntimeAdapter,
5254
TranscriptDiscoveryAdapter,
@@ -95,6 +97,7 @@ class CodeAlmanac:
9597
control: ControlService
9698
engine_runs: EngineRunsService
9799
local_hooks: LocalHooksService
100+
source_bundles: SourceBundlesService
98101
worker_workspaces: WorkerWorkspacesService
99102
workspaces: WorkspacesService
100103
wiki: WikiService
@@ -138,6 +141,7 @@ def create_app(
138141
local_git_state_probe or GitLocalStateProbe(),
139142
)
140143
engine_runs = EngineRunsService(EngineRunsStore(app_config.run_artifacts_path))
144+
source_bundles = SourceBundlesService(SourceBundlesStore())
141145
local_hooks = LocalHooksService(
142146
local_git_hook_manager or FileLocalGitHookManager(),
143147
)
@@ -214,6 +218,7 @@ def create_app(
214218
local_runs = LocalRunPreparationWorkflow(
215219
control,
216220
worker_workspaces,
221+
source_bundles,
217222
engine_runs,
218223
)
219224
sync = SyncWorkflow(workspaces, sources, runs, ingest, queue, SyncLedgerStore())
@@ -231,6 +236,7 @@ def create_app(
231236
control=control,
232237
engine_runs=engine_runs,
233238
local_hooks=local_hooks,
239+
source_bundles=source_bundles,
234240
worker_workspaces=worker_workspaces,
235241
workspaces=workspaces,
236242
wiki=wiki,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from codealmanac.services.control.models import RecordTriggerEventResult
2+
from codealmanac.services.control.ports import LocalGitStateProbe
3+
from codealmanac.services.control.requests import (
4+
RecordCurrentGitTriggerRequest,
5+
RecordLocalTriggerRequest,
6+
)
7+
from codealmanac.services.control.store import ControlStore
8+
9+
10+
def record_current_git_trigger(
11+
store: ControlStore,
12+
local_git_state: LocalGitStateProbe,
13+
request: RecordCurrentGitTriggerRequest,
14+
) -> RecordTriggerEventResult:
15+
state = local_git_state.read(request.cwd)
16+
if (
17+
not state.available
18+
or state.repository_root is None
19+
or state.branch_name is None
20+
or state.head_sha is None
21+
):
22+
return RecordTriggerEventResult(
23+
recorded=False,
24+
reason="git_state_unavailable",
25+
)
26+
return store.record_local_trigger(
27+
RecordLocalTriggerRequest(
28+
repository_root=state.repository_root,
29+
branch_name=state.branch_name,
30+
kind=request.kind,
31+
head_sha=state.head_sha,
32+
previous_head_sha=request.previous_head_sha,
33+
payload_ref=request.payload_ref,
34+
)
35+
)

src/codealmanac/services/control/models.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class ControlRunEventKind(StrEnum):
4646
ERROR = "error"
4747

4848

49+
class SessionProvider(StrEnum):
50+
CODEX = "codex"
51+
CLAUDE = "claude"
52+
53+
4954
class RepositoryRecord(CodeAlmanacModel):
5055
id: str
5156
provider: str
@@ -222,6 +227,70 @@ def require_message(cls, value: str) -> str:
222227
return required_text(value, "run event message")
223228

224229

230+
class SessionRecord(CodeAlmanacModel):
231+
id: str
232+
provider: SessionProvider
233+
provider_session_id: str
234+
source_ref: str
235+
started_at: datetime | None = None
236+
ended_at: datetime | None = None
237+
created_at: datetime
238+
updated_at: datetime
239+
240+
@field_validator("id")
241+
@classmethod
242+
def require_id(cls, value: str) -> str:
243+
return required_text(value, "session id")
244+
245+
@field_validator("provider_session_id")
246+
@classmethod
247+
def require_provider_session_id(cls, value: str) -> str:
248+
return required_text(value, "provider session id")
249+
250+
@field_validator("source_ref")
251+
@classmethod
252+
def require_source_ref(cls, value: str) -> str:
253+
return required_text(value, "session source ref")
254+
255+
256+
class TurnRecord(CodeAlmanacModel):
257+
id: str
258+
session_id: str
259+
provider_turn_id: str | None = None
260+
sequence: int
261+
created_at: datetime
262+
metadata_json: str
263+
264+
@field_validator("id")
265+
@classmethod
266+
def require_id(cls, value: str) -> str:
267+
return required_text(value, "turn id")
268+
269+
@field_validator("session_id")
270+
@classmethod
271+
def require_session_id(cls, value: str) -> str:
272+
return required_text(value, "turn session id")
273+
274+
@field_validator("provider_turn_id")
275+
@classmethod
276+
def require_optional_provider_turn_id(cls, value: str | None) -> str | None:
277+
if value is None:
278+
return None
279+
return required_text(value, "provider turn id")
280+
281+
@field_validator("sequence")
282+
@classmethod
283+
def require_positive_sequence(cls, value: int) -> int:
284+
if value < 1:
285+
raise ValueError("turn sequence must be positive")
286+
return value
287+
288+
@field_validator("metadata_json")
289+
@classmethod
290+
def require_metadata_json(cls, value: str) -> str:
291+
return required_text(value, "turn metadata json")
292+
293+
225294
class ClaimNextTriggerResult(CodeAlmanacModel):
226295
claimed: bool
227296
reason: str | None = None

0 commit comments

Comments
 (0)