Skip to main content

Crate yah_qed

Crate yah_qed 

Source
Expand description

QED — CI scheduler: pipelines, step DAGs, triggers, and pass/fail gating over task execution

QED is yah’s CI layer. It schedules named pipelines, gates on results, and chains into yubaba (deployment) and almanac (data scheduler). Unlike task (execution primitive), qed handles definition, ordering, gating, and triggering.

@yah:ticket(R299-T2, “Wire qed subcommand into CLI”) @yah:at(2026-05-23T01:43:09Z) @yah:status(review) @yah:parent(R299) @yah:next(“Add ‘qed’ variant to app/yah/cli/src/cli.rs Commands enum”) @yah:next(“Create qed subcommand handler in app/yah/cli/src/ (qed.rs or inline)”) @yah:next(“Route ‘yah qed ’ to PipelineLoader + PipelineRunner”) @yah:next(“Verify: cargo check -p yah clean, yah qed –help shows subcommand”)

@yah:ticket(R299-T11, “Scaffold crates/yah/qed crate (types, runner, config loader)”) @yah:at(2026-05-23T20:02:24Z) @yah:status(review) @yah:assignee(agent:claude) @yah:parent(R299) @yah:handoff(“Duplicate of T5 — scaffold was already complete. builtins.rs extracted (T6), .yah/qed/ created (T4), CLI runner wired (T2 gap). cargo check -p qed -p yah clean, cargo test -p qed 3/3 pass.”)

@yah:relay(R407, “QED: native-tarball output + musl-static gate”) @yah:at(2026-06-02T03:25:15Z) @yah:status(open) @yah:phase(P2) @yah:parent(Q405) @arch:see(.yah/docs/working/W154-yubaba-dual-runtime.md)

@yah:ticket(R407-T1, “QED catalog: add ‘produces’ field (oci-image | native-tarball | both)”) @yah:assignee(agent:claude) @yah:at(2026-06-02T03:27:27Z) @yah:status(review) @yah:phase(P1) @yah:parent(R407) @arch:see(.yah/docs/working/W154-yubaba-dual-runtime.md) @yah:handoff(“Added ProduceTarget enum (oci-image | native-tarball) and produces: Vec field to CatalogEntry. Defaults to [oci-image] for container-first safety per W154. Empty produces lists are rejected via CatalogError::EmptyProduces. Re-exported ProduceTarget from images mod + crate root. 5 new tests cover default-when-omitted, explicit native-tarball, both-targets, empty-rejected, and unknown-variant-rejected; all bundled entries verified to default to [oci-image] (no catalog.toml edits needed). compile.rs test fixture updated. 16/16 images::catalog::tests pass.”) @yah:verify(“cargo test -p qed –lib images::catalog::tests”) @yah:gotcha(“Pre-existing unrelated qed test failures: config::tests::parses_build_image_step_from_toml (PushRequiresWritableRegistry on ghcr.io — fallout from in-flight registries module) and tests::test_builtin_release_build_pipeline (asserts 4 steps; release-build now has 6). Neither touches catalog.”)

@yah:ticket(R435-F2, “Runner gates kicks on placement: CLI refuses ci-only without –force; GHA warns/refuses local-only”) @yah:assignee(agent:claude) @yah:at(2026-06-04T19:15:58Z) @yah:status(review) @yah:phase(P1) @yah:parent(R435) @yah:next(“Detect CI via $CI / $GITHUB_ACTIONS at run entry”) @yah:next(“Refuse ci-only from non-CI host unless –force; emit a clear error pointing to the placement field”) @yah:next(“Warn (don’t refuse) when local-only runs on CI — drop a hint to flip placement or split the recipe”) @yah:next(“Decision matrix in W155 is the canonical truth table — encode it in one place”) @yah:verify(“Unit test: each (placement × runner) cell from W155’s matrix routes correctly”) @yah:verify(“Manual: yah qed run yubaba-release from a laptop emits the refusal; --force bypasses with a warning”) @arch:see(.yah/docs/working/W170-qed-recipe-discipline.md) @yah:depends_on(R435-F1) @yah:handoff(“F2 complete. Single source of truth lives at crates/yah/qed/src/placement_gate.rs: RunnerEnv::detect() (reads $CI / $GITHUB_ACTIONS), evaluate(placement, env, force) -> GateOutcome (Allow{warning} | Refuse{reason}). All 6 matrix cells + the –force escape hatch are encoded once. Gate runs at BOTH entry points: (1) CLI yah qed run (app/yah/cli/src/qed.rs) before the camp-proxy probe — fails fast without a daemon round-trip; (2) camp daemon qed_run_handler (app/yah/cli/src/camp.rs) as defence in depth for direct JSON-RPC callers (desktop Run button, agent tools). Added force: Option<bool> to rpc::QedRunParams and threaded through 4 wire construction sites (qed.rs CLI, desktop/qed.rs, agent-tools/qed_tools.rs, all 16 camp.rs test sites). Added –force flag to the Run subcommand. Tests in placement_gate::tests cover all 6 cells, the –force flip, and env_truthy canonical values (9 new tests, all green). Manual verify against /tmp fixture passed all 3 cases: (a) yah qed run ci-thing on Local without –force → Error + exit 1 + clear reason; (b) same + –force → stderr warning + run proceeds; (c) CI=true → silent allow. Full qed lib suite: 165 pass (up from 156); the lone pre-existing test_builtin_release_build_pipeline failure is the same one flagged in R380-T3’s handoff — unrelated to this work. cargo check --workspace clean.”) @yah:next(“R435-T3 can start: stamp placement on the 3 existing recipes (desktop-local=local-only, pond-smoke=anywhere, yubaba-release=ci-only) and audit concurrency_key per W155 principle 3. The gate is live so yubaba-release will start refusing local kicks the moment the field is added — expected and intentional.”) @yah:cleanup(“Surface placement in yah qed list/tail headers — still deferred from F1, equally easy to graft into either F2 or T3.”)

@yah:ticket(R438-T4, “Recipe TOML loader for .yah/qed/transforms/*.toml”) @yah:assignee(agent:claude) @yah:at(2026-06-04T21:07:00Z) @yah:status(review) @yah:phase(P1) @yah:parent(R438) @yah:next(“New loader (separate from pipeline loader) under qed/ that parses transform recipes”) @yah:next(“Recipe schema: name, label, placement { location, runtime }, image (digest-pinned), steps[]”) @yah:next(“Fixed IO contract: YAH_TRANSFORM_IN_0 + YAH_TRANSFORM_OUT env vars; params substitute as {{key}}”) @yah:next(“Argv-element-granularity substitution (no shell, no string concat)”) @yah:verify(“Sample recipe round-trips through loader”) @yah:verify(“Recipe without @sha256: image digest rejected at load”) @yah:gotcha(“Separate dir from .yah/qed/.toml — W164 OQ#1 resolved. Don’t conflate with R435 pipeline-discipline loader.”) @arch:see(.yah/docs/working/W164-derived-static-assets.md) @yah:handoff(“New crates/yah/qed/src/transforms.rs holds TransformRecipe + RecipePlacement + RecipeLocation + RecipeStep + TransformRecipeLoader + substitute_argv + RecipeError. Separate from PipelineLoader per W164 OQ#1 — recipes have a fixed IO contract pipelines don’t. Re-exported from qed lib.rs alongside ENV_TRANSFORM_IN_0/ENV_TRANSFORM_OUT constants for callers (T5 materialize step). Recipe TOML uses the W164 example shape but with image= BEFORE [placement] — gotcha: TOML scopes a scalar that follows a [table] header into that table, so the W164 doc’s example as-written would put image inside placement. Doc-comment in the test fixture and a one-line callout in the substitute_argv helper note this; W164 doc could use a sentence on order. Digest-pin enforcement is two-layered: string-form image=‘…’ rejected at serde-deserialize by ImageRef’s custom Deserialize (T3) — surfaces as RecipeError::Parse; struct-form [image] without digest rejected post-parse by the loader as RecipeError::ImageNotPinned. Argv substitution is element-granular (no shell, no concat); unknown keys preserved verbatim so callers can detect missing bindings; whitespace inside {{ key }} trimmed; unterminated {{ kept literal. 9 transform tests cover round-trip, both digest-reject paths, NotFound, sub-known/unknown/space-preserving/unterminated/trimmed-key. cargo check -p qed -p workload-spec -p cloud clean. Pre-existing test failures unrelated to T4: tests::test_builtin_release_build_pipeline (qed builtins drift, flagged in T4 gotcha) and runner crate uncompiled due to in-flight ‘wip yah dictate + cloud ops’ work.”)

@yah:relay(R487, “Native Rust GHA YAML runtime + action overrides (W200)”) @yah:at(2026-06-08T02:51:53Z) @yah:status(open) @yah:parent(Q486) @yah:next(“Phase order: F1 parser -> F2 expr -> F3 graph/matrix -> F4 step exec + override registry stub -> F5 generic overrides -> F6 docker family -> F7 R2 release override -> F8 cosign -> F9 StepKind::GhaWorkflow”) @yah:gotcha(“v1 has no JS-action runtime; every uses: in release.yml MUST be overridden or the run fails loudly”) @yah:gotcha(“macos-latest jobs hard-error when no mac host is available — surface this in operator docs”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md)

@yah:ticket(R487-F1, “qed-gha crate scaffold + GHA YAML parser (round-trip release.yml/ci.yml/smoke.yml)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:52:30Z) @yah:status(review) @yah:phase(P1) @yah:parent(R487) @yah:next(“New crate crates/yah/qed-gha — workspace member, serde_yaml + thiserror deps”) @yah:next(“Public types: Workflow, Job, Step (post-parse, not raw YAML); Step exposes uses-slug + ref + with-inputs + run-body + env + if + id + name”) @yah:next(“expr.rs: tokenize ${{ … }} substrings inside strings, preserve raw outside (eval is F2)”) @yah:verify(“cargo test -p qed-gha — parse every .github/workflows/.yml round-trip”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:tier(Warrior) @yah:handoff(“F1 landed: crates/yah/qed-gha scaffolded (serde_yaml + indexmap + thiserror). Public types Workflow / Triggers / Job / Step / StepAction / ExprString match W200 §Architecture for the F1 surface — every field release.yml/ci.yml/smoke.yml/smoke-sweeper.yml uses is named, with unknown sibling keys tolerated (parser is permissive in F1; F2/F3 tighten as needed). ExprString tokenizes ${{ … }} into Literal/Expr tokens; expression body is preserved verbatim for F2’s parser. Step::Uses splits slug @ ref. Permissions handled as string (read-all/write-all) or Scopes map. Strategy.matrix keeps raw serde_yaml::Value for include/exclude/dimensions — F3 will normalize into MatrixRows. 15/15 tests green: 8 expr tokenizer unit tests (pure-literal, pure-expr, mixed, fallback-chain, string-quote braces, two-expr-with-literal, yaml scalar coercion, unterminated tail) + 7 round-trip tests covering the four workflows + step-uses split + with-inputs expressions + multiline run bodies. Added to workspace members + default-members. No changes to existing crates.”) @yah:next(“F2 picks up against this surface: implement crate::expr module — Pratt parser for the body strings sitting in ExprToken::Expr, Context lookup (github./matrix./needs..outputs./steps..outputs./secrets./env.*), operators &&/||/==/!=, status functions always()/success()/failure()/cancelled(). Re-export an eval() entry point.”) @yah:verify(“cargo test -p qed-gha”)

@yah:ticket(R487-F2, “GHA expression engine: parser + evaluator + Context (subset used by release.yml)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:52:42Z) @yah:status(review) @yah:phase(P2) @yah:parent(R487) @yah:next(“Pratt parser for Expr AST: Literal, Lookup (dotted path), BinOp (&&, ||, ==, !=), Call (always/success/failure/cancelled)”) @yah:next(“Context struct: github, matrix, needs, steps, env, secrets — secrets is a provider trait (lazy + scope-checked)”) @yah:next(“|| overload: logical AND string-fallback in one operator (matches GHA semantics)”) @yah:verify(“Catalogued expression-shape table from W200 audit: each shape evaluates correctly against a fixture Context”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F1) @yah:tier(Warrior) @yah:handoff(“F2 landed: crate::expr module with tokenizer, recursive-descent parser (precedence: or < and < cmp < unary < primary), and tree-walking evaluator against Context. Value type is JSON-shaped (Null/Bool/Number/String/Array/Object via IndexMap) and shared between AST literals, context payloads, and eval results. Context namespaces: github, env, vars, matrix (Option), needs, steps, inputs, runner, secrets, job. Default Context::new() pre-populates empty objects so missing-path lookups return Value::Null rather than erroring. — Operators: &&, ||, ==, !=, <, <=, >, >=, unary !. && and || short-circuit AND preserve values (GHA semantics: a && b || '' -> b when both truthy, '' when a falsy). Cross-type ==/!= coerces (number<>string via parse, bool<>number via 0/1, bool<>string via ‘true’/‘false’). Identifiers allow - so needs.image-yah-base.outputs.digest parses. — Functions: always/success/failure/cancelled (track ctx.job_status, default Success), contains (string-in-string + item-in-array), startsWith, endsWith, format (GHA {N} indexed holes + {{ }} escapes), join, toJSON, fromJSON (hand-rolled JSON in/out to avoid pulling serde_json into the leaf), hashFiles (delegates to ctx.hash_files host hook; defaults to ‘’). — 24 new tests (39 total in qed-gha). Audit coverage: smoke’s gnarly (github.event_name == 'push' && !contains(github.ref_name, '-')) || (github.event_name == 'workflow_dispatch' && inputs.skip_smoke != true) exercised across 4 truth-table cases; image-gate always() && needs.smoke.result != 'failure' && needs.smoke.result != 'cancelled' across success/skipped/failure/cancelled; needs.X.outputs.Y dotted lookup; inputs.induce_panic == true && '1' || '' string fallback; matrix.use_target_flag fallback. cargo test -p qed-gha = 39/39, no warnings.”) @yah:next(“F3 picks up against this surface: build the job graph from Workflow.jobs (topo via Job.needs), evaluate Job.if_cond and per-step if_cond at scheduling time using crate::expr::evaluate, expand strategy.matrix (dimensions x include, minus exclude) into per-row job instances. No step execution yet — F3 proves order, output propagation between jobs (needs.X.outputs.Y populated from completed jobs’ Job.outputs after expr eval), and matrix fan-out.”) @yah:verify(“cargo test -p qed-gha”)

@yah:ticket(R487-F3, “GHA job graph + matrix expansion + scheduler skeleton (no step exec yet)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:52:52Z) @yah:status(review) @yah:phase(P3) @yah:parent(R487) @yah:next(“Topological sort over needs:; run in waves; propagate needs.X.outputs/result into expression context”) @yah:next(“Matrix expansion: include-only (release.yml shape) is straightforward; full cartesian a few extra lines”) @yah:next(“fail-fast: false handling — row failure does not cancel siblings”) @yah:next(“if: evaluated after needs resolves so always() / needs.X.result work as expected”) @yah:verify(“Fixture release.yml: dry-run produces the expected job order + matrix fan-out + skip-mask”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F2) @yah:tier(Warrior) @yah:handoff(“F3 landed: crate::graph with topological scheduling, matrix expansion, and needs/outputs propagation. topo_sort() returns waves via Kahn’s algorithm with cycle and unknown-needs detection; declaration order preserved inside each wave for diff-stable output. expand_matrix() handles GHA’s 3-step semantics: cartesian over dimensions (declaration order, deterministic) -> apply include rows (merge into matching anchor with non-overwriting new keys, else append standalone) -> drop exclude rows. plan(&Workflow) -> Plan { waves: Vec<Vec> } stitches the two together; matrix rows become per-instance JobInstances with stable job#row keys. — build_needs_value(&[CompletedInstance]) aggregates rows of the same job_id into a single needs.<job_id> entry: result via JobResult::aggregate (failure > cancelled > skipped > success), outputs unioned (later instances clobber earlier). build_context_for_instance() composes the per-instance Context: matrix from the JobInstance row, needs from completed, env from workflow+job env (job shadows workflow), runner.os passthrough. should_run_job() evaluates Job.if_cond as an implicit expression (whole body parsed as expr regardless of ${{ }} delimiters — the GHA semantic that bit on first pass); evaluate_outputs() walks Job.outputs ExprString templates against a steps-populated context. eval_exprstring() helper: single Expr token preserves typed Value, mixed tokens concatenate via as_str_lossy. — 16 new tests, 55 total in qed-gha. Coverage: 2-wave topo + cycle + unknown-needs; 2x2 cartesian, include-only mirror of release.yml’s cli-release shape, include-extends-matching-combination, exclude-drops-matching-row; plan() across topo+matrix; JobResult::aggregate priority; needs.X.result/outputs propagation including matrix-failure aggregation; the real if: always() && needs.smoke.result != 'failure' && needs.smoke.result != 'cancelled' gate against synthetic completion; outputs eval against steps context; ExprString-eval typed-preserving vs. mixed-concatenate split. — F3 verify: plan(real release.yml) gives [smoke] in wave 0, image-yah-base/rust/etc. in wave 1, image-yah-rust-bun in a later wave behind image-yah-rust, and cli-release expands to 3 matrix instances. cargo test -p qed-gha = 55/55, no warnings.”) @yah:next(“F4 picks up against this surface: build the step executor. StepKind::Run (bash) executes via tokio::process with env injection from build_context_for_instance + ::set-output:: capture into ctx.steps. OverrideRegistry stub: trait Override { fn slug(&self) -> &str; fn run(&self, with: &IndexMap<String, Value>, ctx: &mut StepContext) -> Result; }, an unknown-uses lookup errors with the W200 ‘no override registered for X’ message. Hook into Plan::iter_instances + should_run_job: for each instance evaluate if_cond, run steps (Run + Uses dispatch), collect ::set-output:: into ctx.steps, evaluate Job.outputs at the end, feed back into build_needs_value for the next wave. Test fixture: a workflow with one run: step setting an output + one downstream run: step echoing it through ${{ steps.X.outputs.Y }} env injection.”) @yah:verify(“cargo test -p qed-gha”)

@yah:ticket(R487-F4, “Step execution: run: (bash) + OverrideRegistry stub + unknown-action-is-error policy”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:53:01Z) @yah:status(review) @yah:phase(P4) @yah:parent(R487) @yah:next(“shell.rs: run: blocks execute bash with env injection (workflow env > job env > step env precedence)”) @yah:next(“OverrideRegistry: trait + TOML loader (.yah/qed/gha-actions.toml + ~/.yah/qed/gha-actions.toml overlay)”) @yah:next(“v1 policy: uses: a slug with no override registered = loud error (no JS-action runtime)”) @yah:next(“deny + deny_message in TOML overrides specific slugs with a custom message”) @yah:verify(“End-to-end: a no-uses workflow (only run: steps) executes through the runtime”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F3) @yah:tier(Warrior) @yah:handoff(“F4 landed: step executor (crate::runtime) + override registry (crate::overrides) + workflow walker. — OverrideRegistry: trait Override { execute(&OverrideCall) -> Result<OverrideOutcome, String> } + IndexMap-backed registry with three-state Lookup (Found{ovr, config} / Denied{message} / Unknown). load_toml_str()/load_toml_file() parses the W200 schema ([overrides."slug"] { deny=bool, deny_message=str, config=toml-table }); deny wins over registered impl so camps can prevent built-ins. Per-slug config blob is lowered toml::Value -> expr::Value so F5+ overrides read it through the same tree-walker the evaluator uses. Missing TOML files are silent OK (per-camp + per-machine overlays are both optional). default_overlay_paths() returns the W200-canonical .yah/qed/gha-actions.toml + ~/.yah/qed/gha-actions.toml. — Executor: workspace + registry + github/inputs/runner_os + env_passthrough flag (off = hermetic for tests). execute_workflow() walks plan() waves sequentially (concurrency deferred — not a correctness concern), running each instance through run_instance(): build_context_for_instance → should_run_job (skip -> JobResult::Skipped, no steps) → step loop. Step env composes workflow.env + job.env + step.env (step shadows) plus the prior-step $GITHUB_ENV overlay; ctx.steps refreshes each iteration so ${{ steps.X.outputs.Y }} sees prior step outputs. step.if_cond evaluated as implicit expression (whole body parsed as expression); when no if and a prior step failed, defaults to skip — always()/failure()/cancelled() let downstream steps opt in. step.continue_on_error lets the job keep running on step failure. — Bash exec: writes the run-body to a tempfile (set -eo pipefail prelude), spawns bash {file} with composed env + GITHUB_OUTPUT/GITHUB_ENV/GITHUB_STEP_SUMMARY/RUNNER_OS. Captures: ::set-output name=K::V (legacy) + $GITHUB_OUTPUT K=V (modern) + K<<EOF\n…\nEOF (heredoc, with user-chosen delim). $GITHUB_ENV updates fold into env_overlay for subsequent steps in the same job. continue-on-error honoured at step level. — Uses dispatch: with: inputs evaluate through ExprString eval against step ctx (→ typed Value), then route through registry.lookup(slug). Unknown → RuntimeError::UnknownAction with the W200 message (‘no override registered for X — register a built-in or add a TOML deny rule (W200 policy: every uses: must be overridden)’). Denied → RuntimeError::DeniedAction{slug, message}. Found → ovr.execute(&OverrideCall{slug, git_ref, with, env, workspace, config}). — Job outputs evaluate via graph::evaluate_outputs against final steps ctx; CompletedInstance fed into next wave’s needs.* via existing build_needs_value. — 15 new tests (70 total in qed-gha): bash legacy ::set-output capture, $GITHUB_OUTPUT single + heredoc, bash failure -> JobResult::Failure, continue-on-error keeps job Success while step records Failure, $GITHUB_ENV propagates between steps, build->publish output flow through needs.* env injection (the verify-line case), uses unknown -> W200 error, uses registered override receives ExprString-evaluated with: inputs, deny surfaces message, skipped-job propagates through needs.X.result, plus 5 in overrides module (lookup unknown/registered/denied/config-blob/missing-file). cargo build / cargo test -p qed-gha: 70/70, no warnings.”) @yah:next(“F5 picks up against this surface: register built-in Override impls in a new crate::overrides::builtin module — actions/checkout (native git clone into ${workspace}; honour with: { repository, ref, path }); actions/cache (local-fs backend, key/path from with:, cache dir from config.dir, no-op when config.backend == ‘no-op’); actions/upload-artifact + actions/download-artifact (writes/reads a workspace-scoped artifact dir keyed by name); Swatinem/rust-cache (wraps actions/cache impl with rust-toolchain-aware key derivation); dtolnay/rust-toolchain (shells rustup toolchain install + rustup target add from with: { toolchain, targets }); oven-sh/setup-bun (shells bun --version || install). Each impl gets a fixture test exercising at least one with: permutation; ship a register_builtins(&mut OverrideRegistry) entry point and call it from Executor::new() (or a feature-gated path) so a workflow with no uses-overrides-needed runs straight through.”) @yah:verify(“cargo test -p qed-gha”)

@yah:ticket(R487-F5, “Override impls: checkout, cache, upload/download-artifact, Swatinem/rust-cache, dtolnay/rust-toolchain, oven-sh/setup-bun”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:53:11Z) @yah:status(review) @yah:phase(P5) @yah:parent(R487) @yah:next(“actions/checkout — native git clone, respects with.ref / with.repository”) @yah:next(“actions/cache + Swatinem/rust-cache — local-fs backend keyed on with.key + with.path”) @yah:next(“actions/upload-artifact + actions/download-artifact — workspace-scoped artifact dir, paired by name”) @yah:next(“dtolnay/rust-toolchain + oven-sh/setup-bun — shell rustup / bun install respecting with: inputs”) @yah:verify(“Build-only subset of release.yml (cli-release legs without docker/cosign/upload) runs end-to-end against this runtime”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F4) @yah:tier(Cleric) @yah:handoff(“F5 landed: six built-in Override impls + register_builtins(&mut OverrideRegistry) wired into Executor::new(). Tests=77/77. New impls: actions/checkout (no-op when no with.repository; native git clone otherwise honoring ref/path/fetch-depth, target wipe before clone); actions/cache (local-fs backend keyed on with.key+with.path, restore-only, config.backend=no-op short-circuits, config.dir overlays default ${HOME}/.cache/yah-qed/gha); Swatinem/rust-cache (target/ restore keyed on rustc-version+Cargo.lock-digest+with.shared-key+with.key, single workspace honored from with.workspaces); actions/upload-artifact + actions/download-artifact (workspace-scoped ${workspace}/.qed-artifacts//, paired by name; download falls back to all-artifacts-by-subdir when name unset; missing upload path raises loud error); dtolnay/rust-toolchain (rustup toolchain install <ref|with.toolchain> –profile minimal + target add for each with.targets CSV + component add for with.components, cachekey output combines toolchain+cargo version); oven-sh/setup-bun (verify-only: bun –version, errors with install hint if absent; outputs bun-version + bun-path). Executor::bare() preserved for hermetic F4-style tests. v1 limitation documented in code: cache + rust-cache restore-only — save defers to post-step hooks (not in scope for F5). Build-only subset of release.yml is now runnable end-to-end up to (but not including) the docker family covered in F6.”) @yah:next(“User: verify F5 against your operational expectations — especially (a) the restore-only cache semantics (no v1 post-step save — OK for build-only subset, but flag if you want eager save), (b) the actions/checkout no-op-when-no-repo policy (right for the common uses: actions/checkout@v4 case, but skip-vs-error if someone sets only with: { ref } without repository), and (c) the dtolnay/rust-toolchain @ref-as-toolchain fallback. Pickable next: R487-F6 (docker family with registry redirect).”)

@yah:ticket(R487-F6, “Docker override family: setup-buildx, setup-qemu, login, build-push (registry redirect)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:53:19Z) @yah:status(review) @yah:phase(P6) @yah:parent(R487) @yah:next(“setup-buildx / setup-qemu — no-op when host already has them; otherwise shell install”) @yah:next(“docker/login-action — read camp creds for the resolved registry (post-redirect); ignore secrets.GITHUB_TOKEN”) @yah:next(“docker/build-push-action — apply registry_route config (ghcr.io -> registry.yah.dev), emit ProducedArtifact (image digest)”) @yah:verify(“Image-build subset of release.yml (image-yah-base/rust/rust-bun) runs locally and pushes to registry.yah.dev”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F5) @yah:tier(Cleric) @yah:handoff(“F6 landed: docker family overrides (setup-buildx, setup-qemu, login, build-push) with TOML-driven registry redirect. Tests=84/84 (77 prior + 7 new). docker/setup-buildx-action: verify-only via docker buildx version, errors with install hint if absent. docker/setup-qemu-action: verify-only via docker version, assumes pre-installed binfmt (privileged –rm container pull is too heavy + too magical for v1; cross-arch build failures surface at build-push instead). docker/login-action: applies redirect_registry(with.registry) BEFORE shelling docker login; empty password (the ${{ secrets.GITHUB_TOKEN }} case — QED doesn’t resolve that secret) becomes skip-with-success so the host’s pre-existing creds carry the push and any real auth failure surfaces at build-push with the registry’s own message rather than a synthetic one here; non-empty password streams via –password-stdin. docker/build-push-action: applies redirect_image_ref per tag (only the host segment swapped, repo+tag/digest suffix preserved verbatim); shells docker buildx build honouring with.{push, load, platforms, file, provenance, sbom, build-args, context}; captures digest + imageid by reading --metadata-file JSON (containerimage.digest + containerimage.config.digest) so steps.build.outputs.digest keeps working for downstream cosign sign + per-binary DIGEST env blocks. push=true with no tags is a loud error. metadata blob also surfaces as steps.build.outputs.metadata for any consumer that wants the whole file. Pure helpers redirect_registry/redirect_image_ref/parse_buildx_metadata/collect_build_args are unit-tested; full docker shell-out is not (would require a docker daemon).”) @yah:next(“User: verify F6 — especially (a) the empty-password skip-with-success behaviour for docker/login-action (right for release.yml’s ${{ secrets.GITHUB_TOKEN }} shape, but flag if you want a hard error when password is unset on a workflow that genuinely needs to authenticate), (b) the setup-qemu verify-only stance (binfmt install deferred to host setup), and (c) that registry_route only rewrites the HOST segment of an image ref (e.g. ghcr.io/yah-ai/yah-base:latest -> registry.yah.dev/yah-ai/yah-base:latest — confirm registry.yah.dev’s path layout matches yah-ai/). Pickable next: R487-F7 (softprops/action-gh-release — R2 publish via ProducedArtifact).”)

@yah:ticket(R487-F7, “softprops/action-gh-release override -> R2 (emits ProducedArtifact, rides existing publish.rs)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:53:28Z) @yah:status(review) @yah:phase(P7) @yah:parent(R487) @yah:next(“Override impl reads with.files glob, writes each as a ProducedArtifact with binary derived from filename stem”) @yah:next(“config.r2.bucket + config.r2.prefix from gha-actions.toml; prefix templated against ${{ github.ref_name }}”) @yah:next(“Aggregator: workflow run rolls up all overrides’ ProducedArtifacts into the parent QED step’s collection (see F9)”) @yah:verify(“Run release.yml’s cli-release/yubaba-release/camp-release legs through W200; staged tree appears under cdn.yah.dev”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F6) @yah:tier(Cleric) @yah:handoff(“F7 landed: softprops/action-gh-release override + ProducedArtifact plumbing end-to-end. Tests=88/88 (84 prior + 4 new). Plumbing: added yah_qed_gha::ProducedArtifact { binary, path, triple } — structurally compatible with qed::types::ProducedArtifact so F9 maps at the qed-runner seam without dragging a qed dep into qed-gha. New produced: Vec field on OverrideOutcome / StepResult / InstanceRun, plus WorkflowRun::produced() aggregator (only successful jobs + only successful steps contribute, so failed legs don’t leak half-baked artifacts into Outcome::Publish). Override: reads with.files line-by-line; each line is a workspace-relative path or single-segment * / ? glob (no **, no character classes — release.yml’s files: are single tokens), expands against the workspace, derives binary from leading dash-segment of stem and triple from trailing segment (cli-v0.8.10-x86_64-unknown-linux-musl.tar.gz → binary=cli, triple=x86_64-unknown-linux-musl). Strips .tar.gz / .tar.xz / .tar.bz2 / .tgz / .zip; falls back to (stem, None) for filenames that don’t match the convention. with.fail_on_unmatched_files=true is loud per release.yml usage. Outputs: upload_url + url (latter shaped as https://cdn.yah.dev/releases/ so workflow steps that read steps.release.outputs.url still get a usable string).”) @yah:next(“User: verify F7 — especially (a) the binary/triple parsing convention (leading-dash-segment + trailing-suffix vs.- — confirm against release.yml’s actual filenames: cli-.tar.gz, yubaba-.tar.gz, camp-*.tar.gz), (b) the choice to surface url as https://cdn.yah.dev/releases/ (path layout must match W160’s publisher), and (c) that filtering produced by successful-jobs-only is the right policy for partial-release failures (alternative: ship whatever shipped so a partial release isn’t lost). Pickable next: R487-F8 (cosign sign override) and R487-F9 (StepKind::GhaWorkflow integration — this is where ProducedArtifact gets mapped to qed::types::ProducedArtifact and rolled into the outer step’s Outcome::Publish).”)

@yah:ticket(R487-F8, “sigstore/cosign-installer + cosign sign override (verify identity regex matches our registry)”) @yah:assignee(agent:claude) @yah:at(2026-06-08T02:53:38Z) @yah:status(review) @yah:phase(P8) @yah:parent(R487) @yah:next(“cosign-installer override — shell install if not present, no-op if present”) @yah:next(“Bare cosign sign –yes runs as a normal run: step; OIDC identity check happens consumer-side”) @yah:next(“Verify task::default_image::pull’s identity regex accepts the new identity from registry.yah.dev signs”) @yah:verify(“After one signed run: YAH_RUST_BUN_DIGEST=sha256: cargo test -p task default_image::pull – –include-ignored passes”) @yah:gotcha(“OPEN QUESTION: cosign signs digests, but the keyless OIDC identity embeds the issuer; if registry change breaks the regex this needs a tweak in default_image.rs”) @arch:see(.yah/docs/working/W200-qed-gha-action-overrides.md) @yah:depends_on(R487-F7) @yah:tier(Cleric) @yah:handoff(“F8 landed: sigstore/cosign-installer override (verify-only, parity with setup-bun / setup-buildx). Tests=88/88 (registration test extended; install paths are too heavy for v1 — see W200 §No external downloads). Two notes baked into the code header for future readers / F9: (1) Identity regex is REGISTRY-AGNOSTIC. The verifier in task::default_image::pull pins --certificate-identity-regexp ^https://github\\.com/yah-ai/yah/\\.github/workflows/release\\.yml@ — keyed on the workflow URL, not the pushed registry. So F6’s ghcr.io → registry.yah.dev redirect needs NO consumer-side change. Open question #1 from W200 resolves: no regex tweak needed. (2) QED-mode signing is a known v1 gap. The bare cosign sign --yes ghcr.io/yah-ai/<name>@${DIGEST} in release.yml is a normal run: step, not a uses:; cosign-installer just ensures the tool is present. In GHA mode keyless OIDC against token.actions.githubusercontent.com mints the identity and the sign succeeds. In QED mode (yubaba / local) there’s no GHA OIDC token, so cosign sign either drops into the interactive browser flow or fails. Wiring a QED-managed OIDC path (workload identity from camp keystore) is out of scope for F8; v1 expectation is releases sign on GHA, QED-side runs treat sign as best-effort (failure logs without blocking pulls — matches release.yml’s existing behaviour). Worth a follow-up ticket if QED-side signed releases become a goal.”) @yah:next(“User: verify F8 — mostly a doc / scope spike. Confirm (a) you’re OK that v1 cosign-installer is verify-only (host must have cosign on PATH; no auto-install), and (b) you accept the QED-mode signing gap as a future ticket rather than blocking F9. Pickable next: R487-F9 (StepKind::GhaWorkflow + QED runner dispatch — the integration phase that maps yah_qed_gha::ProducedArtifact → qed::types::ProducedArtifact and rolls into the outer step’s Outcome::Publish, and surfaces yah qed run release as a one-step pipeline that wraps release.yml end-to-end).”)

@yah:relay(R495, “QED MCP tools: run status, pipeline list, run history”) @yah:assignee(bundle-anthropic-miravel) @yah:at(2026-06-09T02:04:24Z) @yah:status(review) @yah:next(“Expose qed.run_status { run_id } → pipeline name, step statuses, elapsed, outcome”) @yah:next(“Expose qed.pipelines → list defined pipelines with source file + placement”) @yah:next(“Expose qed.runs { limit, pipeline? } → recent run history (depends on run storage being wired)”) @yah:next(“Wire run storage so yah qed list is populated (prerequisite for qed.runs)”) @yah:handoff(“Shipped QED MCP tools across three sites. (1) crates/yah/agent-tools/src/qed_tools.rs: added QedPipelines tool that dispatches QED_PIPELINES RPC to the camp daemon, returns name/label/scope/params_required/step_count/step_names per pipeline. Exported in qed_tools() vec alongside existing QedRun/QedStatus/QedList/QedCancel. (2) app/yah/cli/src/mcp/tools.rs: added Qed CapabilityGroup; updated group_for_name (qed.* → Qed); updated allowed_groups (Relay + Yubaba get Qed); registered 5 Tool entries (qed.run, qed.status, qed.list, qed.cancel, qed.pipelines) with full input_schema in QED CI TOOLS (5) section; added qed.* dispatch arm in call() that routes through agent-tools KgTool impls (same ToolContext pattern as cloud.*). (3) app/yah/cli/src/qed.rs: wired List subcommand to proxy qed.list to camp daemon via hub_dispatch::try_call_camp, prints tabular run history or graceful ‘no daemon’ message; wired Status subcommand to proxy qed.status, prints pipeline/status/steps. Added 3 tests: qed_tools_are_registered, relay_job_includes_qed_tools, chat_job_drops_qed_tools. 67/67 mcp::tools tests pass; cargo check –workspace clean.”) @yah:verify(“cargo test -p yah –lib mcp::tools # 67/67 pass”) @yah:verify(“cargo check –workspace # clean”) @yah:verify(“cargo test -p yah –lib mcp::tools::tests::qed_tools_are_registered # 1/1”) @yah:verify(“yah qed list # with daemon: tabular history; without: ‘no camp daemon’ message”) @yah:verify(“yah qed status <run_id> # with daemon: pipeline+steps; without: clear error”)

Re-exports§

pub use config::ConfigError;
pub use config::GhaWorkflowEntry;
pub use config::LoaderSubPipelineResolver;
pub use config::PipelineLoader;
pub use events::OutputStream;
pub use events::QedEvent;
pub use images::CatalogEntry;
pub use images::CatalogError;
pub use images::CatalogManifest;
pub use images::ProduceTarget;
pub use eject::eject;
pub use eject::freshness as eject_freshness;
pub use eject::validate_ejected;
pub use eject::EjectFreshness;
pub use eject::GeneratedHeader;
pub use eject::ValidateError as EjectValidateError;
pub use export::export_pipeline;
pub use export::Degradation;
pub use export::ExportReport;
pub use import::content_hash;
pub use import::expand_import;
pub use import::ImportExpansion;
pub use import::ImportFreshness;
pub use native::native_tarball_output_path;
pub use native::pack_native_tarball;
pub use native::tarball_stem;
pub use native::CosignSigner;
pub use native::LoggingSigner;
pub use native::NativeTarballManifest;
pub use native::SignedBlob;
pub use native::SigstoreSigner;
pub use nativecross::is_native_cross_target;
pub use nativecross::plan_native_cross;
pub use nativecross::rewrite_build_argv;
pub use nativecross::select_cross_tool;
pub use nativecross::CrossTool;
pub use nativecross::CrossToolUnavailable;
pub use nativecross::NativeCrossPlan;
pub use nativecross::ToolAvailability;
pub use peers::PeerConfig;
pub use peers::PeerConfigError;
pub use peers::PeerEntry;
pub use placement_gate::evaluate as evaluate_placement_gate;
pub use placement_gate::GateOutcome;
pub use placement_gate::RunnerEnv;
pub use platform::arch_of;
pub use platform::detect_host_triple;
pub use platform::gha_runner_arch;
pub use platform::host_native_crossable;
pub use platform::preflight_line;
pub use platform::resolve as resolve_platform;
pub use platform::resolve_placement;
pub use platform::Platform;
pub use platform::PlatformSpec;
pub use platform::Resolution;
pub use ports::workflow_ports;
pub use ports::PortError;
pub use ports::PortInput;
pub use ports::PortOutput;
pub use ports::PortSecret;
pub use ports::WorkflowPorts;
pub use preflight::audit_workspace;
pub use preflight::check_dep_list;
pub use preflight::check_musl_compatibility;
pub use preflight::render_markdown;
pub use preflight::AuditRow;
pub use preflight::MuslPreflightError;
pub use preflight::WorkspaceAudit;
pub use preflight::KNOWN_GLIBC_ONLY_CRATES;
pub use provider::EventLogConfig;
pub use provider::EventLogProvider;
pub use provider::MapSecrets;
pub use provider::NotarizeProvider;
pub use provider::ProviderContext;
pub use provider::ProviderRegistry;
pub use provider::ProviderReport;
pub use provider::ReleaseProvider;
pub use provider::SecretSource;
pub use provider::EVENT_LOG_PROVIDER;
pub use publish::resolve_release_version;
pub use publish::stage_release;
pub use publish::ChannelManifest;
pub use publish::LoggingReleasePublisher;
pub use publish::PublishRequest;
pub use publish::PublishingOutcomeDispatcher;
pub use publish::ReleasePublisher;
pub use publish::StageReport;
pub use registries::extract_registry_host;
pub use registries::RegistryConfig;
pub use registries::RegistryConfigError;
pub use registries::RegistryEntry;
pub use runner::pipeline_needs_offload;
pub use runner::LoggingOutcomeDispatcher;
pub use runner::OutcomeDispatcher;
pub use runner::PipelineRunner;
pub use runner::RunWhere;
pub use runner::RunnerError;
pub use toolchain::detect_host_versions;
pub use toolchain::effective_pins;
pub use toolchain::resolve_pin;
pub use toolchain::version_satisfies;
pub use toolchain::PinResolution;
pub use toolchain::PreflightEntry;
pub use toolchain::Tool;
pub use toolchain::ToolchainPreflight;
pub use toolchain::ToolchainSpec;
pub use transform::transform_workflow;
pub use transform::transform_workflow_src;
pub use transform::FlagKind;
pub use transform::FlagSeverity;
pub use transform::TransformReport;
pub use transform::TransformedStep;
pub use types::new_run_id;
pub use types::sub_pipeline_ref_token;
pub use types::validate_sub_pipeline_graph;
pub use types::GhaWorkflowConfig;
pub use types::ImportConfig;
pub use types::JobRow;
pub use types::ManifestStitchConfig;
pub use types::Outcome;
pub use types::OutputDecl;
pub use types::Pipeline;
pub use types::Placement;
pub use types::ProducedArtifact;
pub use types::QedRunId;
pub use types::QedRunMeta;
pub use types::QedStep;
pub use types::RunStatus;
pub use types::StepActivation;
pub use types::StepKind;
pub use types::StepStatus;
pub use types::StepValidationError;
pub use types::SubPipelineCollect;
pub use types::SubPipelineConfig;
pub use types::SubPipelineError;
pub use types::SubPipelineRef;
pub use types::SubPipelineResolver;
pub use types::Trigger;
pub use types::WaitForConfig;
pub use types::MAX_SUB_PIPELINE_DEPTH;
pub use yah_qed_gha;

Modules§

artifact_local
Single-host artifact store for the qed-gha runtime (R594).
artifact_retrieval
Content-addressed landing for artifacts retrieved off a remote build-worker (R590-F6 leg 2).
config
@yah:ticket(R299-T4, “Create .yah/qed/ pipeline directory”) @yah:at(2026-05-23T01:43:12Z) @yah:status(review) @yah:parent(R299) @yah:handoff(“.yah/qed/ directory created at workspace root. PipelineLoader.list_all() now dedupes built-ins + custom files. Camp TOML overrides built-ins by name.”)
eject
Eject / materialize an imported workflow to hash-stamped TOML (R533-F6, W224).
events
Live per-step events emitted by crate::runner::PipelineRunner as a pipeline executes (R325-F2).
export
Export-with-degradation: QED → GitHub Actions (R533-F8, W224).
image_overlay
Docker push-family image builder for the qed-gha runtime (R594).
images
Container image catalog + layering compiler.
import
The W224 import primitive’s pure core (R533-F1).
matrix
Native matrix expansion (R505) — pipeline + step scope, mirrors GHA’s strategy.matrix semantics so the GHA bridge stays a passthrough.
native
Native-tarball packaging (R407-T2, W154).
nativecross
The NativeCross tier: host-native cross-compilation (R531-F5, W222).
peers
Per-camp peer registry (R494-F1).
placement_gate
@yah:ticket(R435-F2, “Runner gates kicks on placement: CLI refuses ci-only without –force; GHA warns/refuses local-only”) @yah:assignee(agent:claude) @yah:at(2026-06-04T19:15:56Z) @yah:status(in-progress) @yah:phase(P1) @yah:parent(R435) @yah:depends_on(R435-F1) @arch:see(.yah/docs/working/W170-qed-recipe-discipline.md)
platform
Host platform self-detection (R531-T1, W222).
ports
workflow_call port contract (R533-F5, W224).
preflight
Cross-compile preflight (R407-T3, W143, W154).
provider
Vendor release-provider adapter seam (R509).
publish
Release-channel publishing — the producer leg of the almanac releases feed.
registries
Per-camp registry config (R381-T6).
runner
@yah:relay(R325, “QED desktop UI (blank slate) + backend wiring”) @yah:at(2026-05-26T04:07:25Z) @yah:status(open) @yah:phase(P3) @yah:parent(Q321) @arch:see(.yah/docs/working/W063-area-a-ui-design-impl.md)
secrets_bridge
Loads ~/.yah/qed/secrets.toml — a per-user name-bridge from GHA secret names to a yah-vault slot (or, as a CI/headless fallback, an env var). R487 follow-up.
toolchain
Declarative toolchain pinning (R507, W208 pillar 3).
transform
Assisted one-way GHA→QED transformer (R533-F4, W224).
types
@yah:relay(R623, “Migrate QED run history from .yah/jit/qed/.json to turso”) @yah:assignee(bundle-anthropic-miravel) @yah:kind(task) @yah:at(2026-07-23T03:17:22Z) @yah:gotcha(“QED run history is the ODD ONE OUT: task-runs, task-sessions and gnome_queue are all turso-backed (.yah/db/.turso), but qed runs persist as flat per-run files in .yah/jit/qed/ — <run_id>.json + <run_id>.events.jsonl, 342 of them as of 2026-07-21.”) @yah:next(“Migrate persist_qed_run + load_qed_history (app/yah/cli/src/camp.rs) onto a turso store, following the task-runs store shape (oss/qed/crates/task-runs/src/store.rs).”) @yah:next(“Needs a migration story for the 342 existing run files — import-on-boot or a one-shot, not a silent drop.”) @yah:next(“NOT a blocker for R622 (manual steps). R622’s parked-run durability is satisfied by a non-terminal JSON persist reusing the R603 pattern, which a turso migration would carry over anyway. Deliberately decoupled — see W282 ‘The turso question is real, but separate’.”)
waitfor
@arch:see(.yah/docs/working/W207-dashboard-e2e-in-qed.md)

Structs§

ForgeId
Re-exported so daemon glue (camp.rs boot-reconcile, R603-T4) can parse a persisted bare-uuid task_run_id back into the workload identity that PipelineRunner::resume_terminal_publish_for_remote_step takes, without a direct observation dep edge. Stable identity for a forge run (UUID), shared across all three forge species (local, remote, integration). Stable across yah restarts.
RecipePlacement
Where + how a recipe step runs.
RecipeStep
One executable step in a recipe. argv[0] is the executable; the {{key}} placeholders are substituted at element granularity by substitute_argv. There’s no shell — element boundaries are preserved.
TransformRecipe
On-disk recipe shape. Top-level TOML keys map 1:1 to fields.
TransformRecipeLoader
Loader for .yah/qed/transforms/*.toml files.

Enums§

RecipeError
RecipeLocation
Recipe-side location vocabulary (Local-only for W164).
TaskRuntime
How a task is sandboxed. Independent of where it runs — combine with TaskLocation inside TaskPlacement.

Functions§

almanac_dispatch_argv
Returns the argv that an external scheduler (e.g. almanac) should submit as a TaskSpec to dispatch a named pipeline.