Move ColdPath and ToolProvisioning into core; complete api's re-exports - #514
Conversation
`devlaunch_core::api` named a launcher nobody outside `dl` could build. The two implementations that decide whether a launch can go cold at all lived in the binary: `ColdPath`, which opens devlaunch's records, and `ToolProvisioning`, which lends the host's tools in. Both are core types plumbed together; what kept them in `dl` was where their events get printed. So both move, with the event sinks injectable as typed values, and `dl` keeps the printer and the words. The records move with them, into a new `flows::records`. `api` now re-exports every one of `Launch::new`'s parameter types; five of the seven used to live outside it. `ColdRefused` had to be typed for the move to be possible at all: it carried `reason: String`, which was the one place dl's prose travelled back through core, and core cannot write the words. It is now a sum over the startup reasons plus a no-cold-path arm, the shape #313 decided and #339 specifies. `ConfigError` became clonable and comparable to travel inside it, its OS side spelled as `OsFailure` the way `MetadataError`'s already was. Red first: a test constructing a cold-capable `Launch` from `api` paths alone, which failed to compile on eleven unresolved imports. Closes #340.
Reviewer's GuideMoves the real cold-path record handling and tool provisioning into Sequence diagram for lazy cold-path record openingsequenceDiagram
participant Consumer
participant Launch
participant ColdPath
participant Records
participant Sink
Consumer->>Launch: Launch::new(...)
Note over Launch,ColdPath: Construction performs no record I/O
Consumer->>Launch: Launch::run(...)
Launch->>ColdPath: open()
ColdPath->>Records: open_records(runner)
Records-->>ColdPath: Records or StartupError
ColdPath->>Sink: say_all(RecordsNotice events)
ColdPath-->>Launch: Cold or ColdRefused::Startup
Sequence diagram for typed event rendering during tool provisioningsequenceDiagram
participant Launch
participant ToolProvisioning
participant ProvisionFlow
participant Sink
participant DL
Launch->>ToolProvisioning: from_env(cache, notices)
Launch->>ToolProvisioning: provision_tools(runner, workspace_id, occasion, title)
ToolProvisioning->>ProvisionFlow: provision_tools(...)
ProvisionFlow->>Sink: Notices<ProvisionEvent>
Sink->>DL: render provisioning event
ProvisionFlow-->>ToolProvisioning: provisioning result
ToolProvisioning-->>Launch: ClaudeConfig or DevpodMissing
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report❌ Patch coverage is
Additional details and impacted files
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
blooop
left a comment
There was a problem hiding this comment.
This was generated by AI during review.
Reviewed at merge-base 669df03...9925291, fresh context, two independent axes. Every claim in the description was re-derived rather than taken on trust; where I could refute a finding I dropped it and said so.
Standards
Core renders no output: holds, and this PR is the thing that fixes it. Every string literal added anywhere under devlaunch-core/ in this diff is a test fixture, a path, or the expect("the records were just opened") panic text moved verbatim from the deleted cold.rs. No print!/println!/eprintln!, no sentence-shaped format string, no reach into dl::render. The reverse channel is genuinely closed: ColdRefused.reason: String was the only place dl prose travelled back through core, and "the cold path is not available to this caller" now lives at dl/src/render.rs:2580 rather than inside flows/launch.rs. Worth knowing that nothing in test/ enforces this rule; it is review-enforced only.
ConfigError remodel: no information lost. OsFailure (devlaunch-core/src/domain/metadata.rs:107) keeps kind: io::ErrorKind alongside message, and From<io::Error> sets both. Nothing in the tree branched on that arm's kind before or after. Eq is over kind+message, used only in tests, in no HashSet or dedup. Clone is load-bearing, not speculative: ColdRefused derives it. The claim that MetadataError already made this choice in the same words checks out verbatim.
minor — devlaunch-core/src/flows/lifecycle.rs:11. The module doc says "Everything reachable here is binary surface, not part of the frozen wf API (#251 §7), except the three §7 names (list, remove, up)." That sentence was true at the merge base and this PR makes it false: it adds Refresh and SelfInvocation to api, and pub struct devlaunch_core::api::Refresh<'a> is now a declaration in public-api.api.txt.
minor — devlaunch-core/src/flows/records.rs:41 and :78. StartupError and RecordsNotice each carry binary surface, not part of the frozen wf API (#251 §7) in the same commit that re-exports both at api:: and files them in the promise file (public-api.api.txt:76-89). :41's hedge, "on its own; it reaches the promised tier as the payload of ColdRefused", is wrong for the same reason: it is directly at api::StartupError now, not only as a payload. :110 on Records is correct and should stay, which is what makes the other two look like an oversight rather than a convention.
This matters more than a comment usually would. docs/development.md makes the promise/tripwire split the entire basis on which a snapshot diff gets read, and a maintainer who opens flows::records is told these types are free to change.
minor — devlaunch-core/tests/public_api_snapshots.rs:19. Still reads "42 of the 79 rows the generator emits for the api section". Three prose sites were corrected to 133/259 (src/lib.rs:56, scripts/public-api-snapshots.sh:29, docs/development.md:61); this is a fourth carrying the same measured figures from #347, and it is now the only one that disagrees with the files it is documenting. test/test_public_api_snapshots_doc.py asserts nothing about the counts, so there is no tripwire under any of the four.
minor — devlaunch-core/src/flows/launch.rs:2463. The description's gate list says cargo doc --workspace --no-deps "introduces no new broken intra-doc link". It introduces one: public documentation for 'NoColdPath' links to private item 'NoColdPath', from the +-added doc line on the public ColdRefused::NoColdPath variant pointing at the pub(crate) struct. Cosmetic, since ~136 such warnings pre-exist repo-wide, but the gate statement as written is false.
nit — devlaunch-core/src/flows/records.rs:76. "because a sink is what lets the words be said while the work is still happening" is not what this flow does. open_records collects into Records.reported and ColdPath::records() drains it with say_all after the whole open, migration included, has returned. Not a regression, the old session.rs deferred the same way, but notices.rs's module doc opens by naming a Vec as precisely the thing that decides when the saying happens, so the sentence claims the property this one flow gave up.
Refuted and dropped. The RefCell around the provision sink: borrow_mut is scoped inside the provision_tools call and provision::provision_tools is a free function that cannot re-enter the value exclusively borrowing it, so the panic path is unreachable. ColdPath<'r, '_>'s second lifetime: eight elided call sites, mechanical. flows::records as a Middle Man: it owns the ordering function, the notice vocabulary and the single construction point. render::cold_refused and startup_reason are wildcard-free, so a future arm is a compile error. And an earlier draft finding that an external ColdMachinery cannot be written from api alone was wrong for the case the lib.rs comment actually names: a NoColdPath-shaped impl only ever returns Err, which needs nothing beyond api::{ColdMachinery, Cold, ColdRefused}. (Only an impl returning Ok would need WorkspaceCloneManager and MetadataStorage, which api does not re-export. The comment does not claim that case.)
Pre-existing, not this PR. rust/dl/tests/read_side.rs:748 the_json_listing_migrates_the_cache_and_the_table_does_not fails roughly one full-suite run in five and passes in isolation. --ls is in CACHE_READING_COMMANDS (flows/completion_cache.rs:209), so it spawns a background dl --update-cache, that child calls open_records, and the migration it runs races the assertion that the document is still at version 1. Untouched by this diff and worth its own issue.
Spec
Spec is #340, with #313's decision comment as the parent and #339's substance carried as a prerequisite.
Re-export completeness: satisfied, proven rather than read. #340: "complete api's re-exports so every Launch::new parameter type is reachable from api alone (Refresh, Host, Notices<LaunchNotice>, the impls), today 5 of 7 live outside it." Launch::new (flows/launch.rs:3000-3008) takes &mut CommandContext, &mut Refresh, &mut dyn ColdMachinery, &dyn Provision, &Host, &mut dyn FnMut(&str) and &mut dyn Notices<LaunchNotice>. A throwaway external crate with a path dependency on devlaunch-core compiles all seven as aliases through devlaunch_core::api:: (the sixth being std), plus ColdPath and ToolProvisioning coerced to their traits, an exhaustive match on every ColdRefused arm, Vec<T> as each of the three sinks, and a third-party impl ColdMachinery. Nothing required a flows:: path.
The red test is real. Dropped into a merge-base worktree, devlaunch-core/tests/api_launch_is_self_sufficient.rs fails with exactly the claimed E0432 over exactly the eleven claimed names. It imports only through devlaunch_core::api:: plus devlaunch_test_support::FakeRunner, and it builds the real ColdPath::new and ToolProvisioning::from_env, not stubs.
nit — devlaunch-core/tests/api_launch_is_self_sufficient.rs:73. The module doc says "the sinks are still empty when the launcher exists", and the test asserts that of records_said and launch_said but not provision_said, which is borrowed by the live ToolProvisioning. Harmless, since from_env emits nothing, but the assertion list is one short of the sentence above it.
Snapshot honesty: satisfied. public-api.api.txt 37 to 126 with zero deletion lines. public-api.rest.txt 2598 to 2755 with exactly eleven deletions: ten rows of the old ColdRefused struct and its impls, and ConfigError::Unreadable::source changing type. wc -l at both revisions matches every figure in the table. Independently corroborated by CI: the public-api job regenerates the files with the pinned cargo-public-api and diffs them against what is checked in, and it is green, so these were regenerated and not hand-edited. (Trivially, the body reads "the eleven rows of the old ColdRefused ... and ConfigError::Unreadable::source", which parses as twelve; it is ten plus one.)
Byte-identical rendering: satisfied. The base rendered format!("could not read {} ({source})", path.display()), pure Display on io::Error; head renders source.message, and OsFailure::from sets that to error.to_string(). No {:?}, no .kind(), and ConfigError::Unreadable has exactly one render site. NoColdPath's literal moved across byte-for-byte.
Behavioral no-op: satisfied, strongly. git diff --stat over rust/dl/tests/ and test/ is empty. The exact-call-sequence tests in rust/dl/tests/launch.rs were not touched at all, let alone loosened, and no assert or #[test] line is removed anywhere in the diff. commands::report keeps its same four call sites, each paired with a direct open_records, so nothing is said twice. cargo test --workspace is green locally.
Coverage gap, and it is the same gap as #339's. ColdPath::open's Err arm, render::cold_refused and render::startup_reason have zero test coverage, and nothing in the tree constructs StartupError::Metadata, StartupError::Config or ColdRefused::Startup. ConfigError::Unreadable's rendering is untested too. Both "the sentences are unchanged" claims are therefore inspection-only. They hold, I checked them by hand, but nothing would catch it if they stopped holding.
CHANGELOG: satisfied. Two entries under [Unreleased] / Changed, covering the move and the typing, and accurate about both.
On #339
Shape satisfied. The red test #339 names does not exist on this branch.
The shape is a faithful reading and arguably better than the letter. #339 asks for "a sum over the startup reasons, NoHomeDirectory / Config(ConfigError) / Metadata(MetadataError) (payloads already core types, mirroring dl's private StartupError), plus a no-cold-path arm". The branch promotes the real StartupError into core (flows/records.rs:45-49) instead of restating it, so ColdRefused::Startup(StartupError) | NoColdPath matches exhaustively over the same four outcomes. The second clause is met as well: render::startup_reason (dl/src/render.rs:2742) is the match on the typed arms, and NoColdPath's English literal moved out of core.
But #339 also names its own red: "a test pinning that a metadata-refused cold open surfaces as the typed arm, not prose." Searching every .rs under rust/, nothing constructs ColdRefused::Startup or StartupError::Metadata at all. The two closest things are neither of it: flows/records.rs:191 the_four_sources_are_reported_in_the_order_python_produced_them carries a MetadataError, but as a RecordsNotice::MigrationRefused in the notice report rather than as a refusal out of a cold open; and tests/api_launch_is_self_sufficient.rs:30 builds the real ColdPath and deliberately never opens it.
So the description's "a maintainer who reads its spec as satisfied can close it" is accurate only if the maintainer waives the test. Recommendation: keep #339 open on the test alone, and it is cheap to write now that ColdRefused is comparable.
Siblings
No overlapping hunks. #504 also edits dl/src/render.rs and #511 also edits domain/config.rs and flows/launch.rs, but all three are comment-only changes in distant regions. Beyond those, every sibling shares only the [Unreleased] CHANGELOG block. The branch is five commits behind main; main's flows/repo_manager.rs and flows/workspace_clone.rs changes added no rows to the snapshots and its own public-api job is green with them unchanged, so the regenerated rest.txt here stays valid after the merge.
Verdict
Comment. No blocking findings. All fifteen gates in the description that I could check independently hold, including the three the description is easiest to be wrong about: the promise file really does grow by 89 rows and lose none, the rendered sentences really are byte-identical, and no exact-call-sequence test was touched. The layering fix is the substance here and it lands.
What I would fix before merge, none of it structural:
flows/lifecycle.rs:11andflows/records.rs:41/:78, three doc markers that now contradict the tier the same commit put those types in.tests/public_api_snapshots.rs:19, the fourth prose site still at 42/79.- The description's
cargo docgate line, which is false as written.
And separately from this PR: keep #339 open for its red test, and file the read_side.rs:748 flake.
The review found the shape of #339 satisfied and its named red test missing: nothing in the tree constructed `ColdRefused::Startup` or `StartupError::Metadata`, so both "the reason travels as a type" and "the sentences did not move" were inspection-only claims. Three tests now hold them, at the three places the claim lives: - core, `flows::launch`: a metadata-refused cold open arrives at `BranchNotNamed::Cold` and `NotPrepared::Cold` as the reason it is, with the `MetadataError` intact. Proven red by flattening the arm back to a `String`, which stops all three assertions compiling. - `dl::render`: every arm renders the exact line it used to arrive already rendered with, whole strings and not substrings, plus the composition into `Repository 'owner/repo': ...` and the `io::Error` wording that survived `ConfigError` gaining `OsFailure`. - `test/unit/test_cold_path_refusal.py`: a real run whose cache directory is a file, which is the only way to reach `ColdPath::open`'s Err arm. The open resolves its paths from the process environment, so nothing inside either crate can run it. Also the four doc markers the review caught, all made wrong by the previous commit rather than pre-existing: `flows::lifecycle`'s "except the three §7 names", the binary-surface notes on `StartupError` and `RecordsNotice` which the same commit filed in the promise file, and the fourth prose site still at 42-of-79. `Records` gains the note it should have had: reachable through `api::ColdPath::records` without being declared there, which is #352's gap. One rustdoc private-link warning went with them, from the new `ColdRefused::NoColdPath` doc pointing at the `pub(crate)` struct. Measured: 126 such warnings in devlaunch-core before this branch, 125 after.
# Conflicts: # CHANGELOG.md
|
Heads up from the stacked build on top of this branch (#520, #410):
|
# Conflicts: # CHANGELOG.md
Three things needed a hand rather than a textual resolution. CHANGELOG: #500 cut 0.25.0 while this branch was open, so `[Unreleased]` came back empty with the release directly under it, and a clean-reading textual merge files this branch's entry inside the shipped release. Resolved so the entry sits between `[Unreleased]` and `[0.25.0]`, with main's own `Added` and `Fixed` entries kept beside it and nothing deleted. `dl/src/session.rs`: main added `open_storage` to the very block #340 deleted from the binary. The block stays deleted, and `open_storage` is ported to `devlaunch_core::flows::records` beside `open_records`, which is where the rest of that plumbing went and which it is a shorter path through. `dl` now names it from there. One new row in the rest snapshot, none in the promise file. Snapshots regenerated from the merged tree rather than carried across it, and the counts the prose names still hold: 126 promised rows, 259 in the section the filter reads.
Two hand resolutions, both the same shape as the last merge. CHANGELOG: main's `--purge` entry and this branch's are both bullets of the one `Changed` section under `[Unreleased]`. Kept side by side, nothing deleted. `dl/src/commands.rs`: main's new `say_retired_keys` calls `session::worktree_config`, which #340 deleted from the binary along with the rest of the records plumbing. It reads `domain::config::worktree_config` directly now, which is what dl's wrapper forwarded to. Snapshots regenerated from the merged tree: no change, and the promise file is still 126 rows.
|
Thanks. Every finding taken; the review's own recommendation on #339 is the one thing I have gone the other way on, and only because the test it asked for now exists. Pushed as The review found no inline threads to reply on (it is one review body), so this answers all five plus #339 in order. 1.
|
Line length, caught by prek rather than by me: the suite's ruff config fits that signature on one line.
Implements the #313 decision's second build: the real
ColdMachineryandProvisionimplementations move intodevlaunch-core, andapire-exports everythingLaunch::newasks for.The problem, stated as a test
api::Launchwas reachable and not constructible. Five ofLaunch::new's seven parameter types lived outsideapi(Refresh,Host,Notices<LaunchNotice>, and the two implementations), and the two implementations that decide whether a launch can go cold at all lived inside thedlbinary, where nothing butdlcould name them. A second consumer could name the launcher and had nothing to hand it.The red test is
rust/devlaunch-core/tests/api_launch_is_self_sufficient.rs: it builds a cold-capableLaunchwith the realColdPathand the realToolProvisioning, importing every parameter throughdevlaunch_core::apiand nothing else. Before the change it did not compile:Eleven names, one import list. It also asserts the other half of devlaunch#145 at runtime: building a cold-capable launcher spawns nothing, says nothing, and opens no records, which is what makes
ColdPatha way to get the records rather than the records.What moved
dl/src/cold.rsColdPathdevlaunch_core::flows::launch::ColdPathdl/src/launch.rsToolProvisioningdevlaunch_core::flows::launch::ToolProvisioningdl/src/session.rsRecords/open_records/StartupErrordevlaunch_core::flows::recordsThe only thing that kept either implementation in the binary was where its events get printed, so each now takes an event sink as a constructor argument:
ColdPath::new(runner, &mut dyn Notices<RecordsNotice>). The four things the open used to hand back as separate fields (the config's retired keys, the load's notices, the migration report, the migration's refusal) are one vocabulary now, said in the order Python's factory produced them.dlimplementsNotices<RecordsNotice>on the printer it already had.ToolProvisioning::from_env(cache, &mut dyn Notices<ProvisionEvent>). The sink sits behind aRefCellbecauseProvisionanswers through&self; one launch makes one pass at a time, so there is nothing to contend with.dlkeeps the rendering and only the rendering.dl/src/session.rsis down to the two answers only a running process can give: where the cache is, and how to re-run this build.ColdRefusedhad to be typed for any of it to workColdRefusedcarriedreason: String, filled bydlrendering aStartupErrorand quoted back into core's own launch refusal. That is the one place the binary's prose travelled back through core, against #251 section 5, and it is why #313 put the typing first and the move behind it: aColdPathinside core cannot write the words.So this branch does that typing too, in the shape #313 decided and #339 specifies:
StartupErroris core's now rather than mirrored there, so the arms reuse it instead of restating it.dl'srender::cold_refusedis the match, and every sentence a user sees is unchanged. #339 is closed by this PR. Its substance was here from the first commit and its named red test is here now, after review pointed out that nothing in the tree constructedColdRefused::StartuporStartupError::Metadata. See "The typed refusal's own red" below.One consequence worth naming:
domain::config::ConfigErroris nowClone + PartialEq + Eq, because a refusal that travels inside another has to be as copyable as the one carrying it. Its OS side is anOsFailurerather than anio::Error, which is the choiceMetadataErroralready documents and made in the same words.OsFailure::messageisio::Error::to_string(), so the rendered line is byte-identical.The snapshots
Regenerated for real, not hand-edited: nightly plus cargo-public-api 0.52.0 installed in a scratch prefix,
scripts/public-api-snapshots.shrun, and the output confirmed byte-identical to the checked-in files onmainbefore any code changed.devlaunch-core/public-api.api.txtdevlaunch-core/public-api.rest.txtdevlaunch-runner/public-api.txtThe promise file grows by 89 rows and loses none. All 89 are additions, and they are the parameter types the ticket asked for:
ColdMachinery,ColdPath,Cold,ColdRefused,Provision,ToolProvisioning,Host,Refresh,SelfInvocation,Notices, and the three notice vocabularies they are parameterised by (LaunchNotice,ProvisionEvent,RecordsNotice) plusStartupError. The vocabularies bring their variants with them, which is most of the row count: a sink type is not usable by a consumer that cannot match on what it receives.The rest file's diff is the same move seen from the canonical side, which is the documented one-way limit (#352): the newly promised types' constructors, methods and derived impls render at
flows::…and land there. The only deletions in the whole run are eleven rows: ten for the oldColdRefusedstruct and its impls, and one forConfigError::Unreadable::sourcechanging type. Nothing else was removed from either file.Three prose sites carried measured counts from #347 and are corrected in step:
docs/development.md,devlaunch-core's crate docs, and the header ofscripts/public-api-snapshots.sh. The generator emits 259 rows for theapisection now and the filter keeps 126; it was 79 and 37.Gates
cargo test --workspacegreen, 29 suites, including the two new unit tests over the records report's ordering and the partition invariant over the regenerated snapshots.cargo clippy --locked --all-targets -- -D warningsclean.cargo fmt --checkclean.DEVLAUNCH_DL_CMD='cargo run …' pixi run pytest test/green: 427 passed, 6 skipped.cargo doc --workspace --no-depsemits one fewer warning than the merge base, measured rather than asserted: 126 private-link warnings indevlaunch-corebefore this branch, 125 after. The first push did introduce one (the newColdRefused::NoColdPathdoc pointing at thepub(crate)struct); review caught it and it is gone. A broken-link warning left behind by the deleteddl::coldmodule is fixed too.[Unreleased].The typed refusal's own red (added after review)
Review found #339's shape satisfied and its named red test absent: nothing in the tree constructed
ColdRefused::StartuporStartupError::Metadata, so "the reason travels as a type" and "the sentences are unchanged" were both inspection-only. Three tests now hold them, one per place the claim lives.flows::launch::tests::a_metadata_refused_cold_open_surfaces_as_the_typed_armdrivesname_default_branchwith a cold path that refuses the way the real one does, and asserts the whole refusal value:BranchNotNamed::Cold(ColdRefused::Startup(StartupError::Metadata(..))), with theMetadataErrorthe store produced still inside it. A sibling pins the same forprepare'sNotPrepared::Cold, because those are two separatemap_errs over oneopen, and a third pins thatNoColdPathrefuses with an arm rather than the English literal it used to carry.Proven red rather than assumed: flattening the arm back to
Startup(String)and filling it withformat!("{refused:?}")inColdPath::openbreaks all three at compile time.render::teststakes the byte-identity claim off inspection. EveryColdRefusedarm is asserted against its whole sentence, not a substring, since acontainswould pass while the line a user reads quietly changed. Two more cover the composition intoRepository 'owner/repo': <reason>and theio::Errorwording that had to surviveConfigError::Unreadableswappingio::ErrorforOsFailure.test/unit/test_cold_path_refusal.pyis the only thing that can runColdPath::open'sErrarm at all. The open resolves its paths from the process environment, so nothing inside either crate reaches it without mutating an environment every other test in the binary shares. A realdlwith a file where its cache directory belongs does reach it, and the suite already scopesXDG_CACHE_HOMEper test.The four doc markers review caught
All four were made wrong by this PR's first commit rather than pre-existing, and all four are the kind that misleads a maintainer reading a snapshot diff:
flows/lifecycle.rs:11said everything there is binary surface "except the three §7 names". This PR putRefreshandSelfInvocationinapi. It now points atapi's re-export list as the authority instead of restating it.flows/records.rsmarkedStartupErrorandRecordsNoticebinary surface in the same commit that filed both inpublic-api.api.txt. Both now say they are promised, and why.Recordskeeps a note of its own, corrected the other way: not re-exported fromapi, reachable throughapi::ColdPath::recordsall the same, which is The api promise file misses Launch::new and Launch::run #352's classifier gap rather than a second tier.tests/public_api_snapshots.rs:19was a fourth prose site still at "42 of the 79 rows". Now 133 of 259, with the other three.Also taken from review: the self-sufficiency test asserted two of the three sinks were empty while its own docstring claimed three. The provisioner is dropped so the third can be read.
Merged with main after 0.25.0 shipped
mainmoved and #500 cut the release, so[Unreleased]came back empty with## [0.25.0] - 2026-08-28directly under it. That is the merge this branch's CHANGELOG entry could have been lost in: a textual resolution files it inside the shipped release, reads cleanly, and is wrong. Resolved by hand and checked two ways,git diff origin/main -- CHANGELOG.mdshows zero deletion lines, and the entry sits between## [Unreleased]and## [0.25.0].All three snapshots were regenerated from the merged tree rather than carried across it, and came out byte-identical to what the merge produced:
main's changes toflows::repo_managerandflows::workspace_cloneadd no rows. Counts unchanged at 126 / 2755 / 202. Gates re-run on the merge, all green.Closes #340
Closes #339
🤖 Generated with Claude Code
Summary by Sourcery
Move cold-launch machinery and records management into core, complete the launcher API re-exports, and carry refusal reasons as typed values while preserving user-facing behavior.
New Features:
Launch::newparameter types and providing the real cold-path and tool-provisioning implementations.Bug Fixes:
dl.Enhancements:
dlintodevlaunch-core, leaving the binary responsible for rendering and process-specific behavior.Documentation:
Tests:
devlaunch_core::apiwithout opening records or spawning processes.Chores: