How Conduit Runs
Onboarding a project, surviving operational failure, and what's deferred until the system grows · ~14 min read ~– min read · Suggested by Bob technicaloperations
An architecture is only as good as the day-to-day operating experience it produces. Conduit's day-to-day is small on purpose. This cairn is the runbook view: how a new project joins, what the action does step by step, where versioning matters, what the failure modes look like, and what is deferred until the system is bigger than two consumers.
Onboarding a new project
Adding a new project to Conduit is four mechanical steps. (1) .conduit/project.json — project metadata such as name, description, color, icon, and repo_url. (2) Workflow YAMLs for pulse, Recap (devblog), ADR, Leadership Brief (brief), Product Updates (product-updates), or manual milestone runs, all calling uses: Constructured/conduit/.github/actions/publish@v4. (3) One repo secret, CONDUIT_DISPATCH_TOKEN, plus one repo variable, AWS_CICD_ROLE_ARN; Bedrock auth comes from GitHub OIDC into AWS, not a checked-in API key. (4) The host repo’s actions/permissions/access set to organization (one-time, invisible in YAML, easy to forget). There is no caller-side .conduit/state.json to maintain anymore.
If onboarding fails with Unable to resolve action ‘constructured/conduit’, repository not found, that’s almost certainly the access setting. Run gh api -X PUT repos/<owner>/<action-host-repo>/actions/permissions/access -f access_level=organization.
What happens on a workflow run
From a single trigger to a published artifact, the run goes through a fixed sequence: install timbers, verify Bedrock access through the same invoke-llm.sh helper used for real drafts, install canonical templates, compute per-type time windows, run timbers query --range and timbers draft <template>, wrap with frontmatter, and build the bundle. If artifact_count > 0, dispatch it; otherwise stay silent. The caller repo is read-only from the action’s point of view. Operational failures surface red after dispatch succeeds for whichever types did work, so partial-success artifacts still land.
Versioning policy
The action now ships at @v4. The Bedrock/OIDC move changed the authentication contract enough to deserve a new major: callers add id-token: write, run aws-actions/configure-aws-credentials, and pass the usual Conduit dispatch token. Patch fixes still retag the current major in place; consumers pinned to @v4 pick them up on their next run with no per-repo PR. Consumers that want stricter immutability can pin to a specific commit SHA (@<40-char-sha>) and accept losing patch fixes until the SHA bumps.
Re-tagging v4 is a force-push to the tag. CI consumers re-resolve the tag at workflow runtime so they pick up the new commit transparently; the blast radius is humans with stale local clones plus possibly some CI caching layers, but never CI execution itself.
Operational failures and how to read them
Three failure modes have shown up in production. The AWS role or model path is bad. The Bedrock probe warns, the draft step fails through invoke-llm.sh, op_failures > 0, and the workflow goes red after any successful artifacts have been dispatched and anchored. Remediation is usually the caller repo’s AWS_CICD_ROLE_ARN, OIDC trust policy, aws_region, or model input. The role is valid but lacks the model. Bedrock returns an access or validation error for the configured model ID or inference profile; the default is global.anthropic.claude-opus-4-7, and least-privilege IAM should allow bedrock:InvokeModel on that profile ARN. The repository_dispatch is rejected. Rare; usually a token-scope issue. Most common subform was the client_payload is not an object 422, fixed by switching from gh api -F to jq | gh api --input -.
Backfill and replay
Two scenarios need to override the natural time-window flow. Backfill after a long quiet period: pulse defaults to 24 hours and the other channels default to seven days, with per-type <TYPE>_WINDOW_HOURS overrides. Re-running from a specific commit: workflows expose since_sha_override so every requested type starts from that SHA instead of its time window. The action validates the SHA up front and compacts oversized ranges by dropping the oldest entries first within a byte budget.
Templates as a shared resource
The canonical timbers prompts ship inside the action and install into the runner’s ~/.config/timbers/templates/ on every run. timbers’ resolution order is project → global → built-in: a project with no .timbers/templates/ of its own gets the canonical Conduit prompts; a project that wants to override one template (say, devblog.md) drops one file in .timbers/templates/ and only that template overrides. Editing voice across all consumers is one PR against the conduit repo plus a v4 retag — every consumer picks it up on the next run.
What’s deferred until the system grows
Several things are deliberately not built yet. Q reviewer support across all repos: automated review coverage is still uneven outside Strike; tracked in beads. Multi-cycle convergence loop: implementer mechanics are written into project rules; reviewer mechanics are still maturing; tracked in beads. Tag-driven release automation: release-notes has been retired in favor of product-updates, but a separate tag-push release lane is still future work. Migration to a conduit-side puller: still only worth doing past five-or-so consumers; the current action keeps each caller simple and read-only. Customer-facing slice: Product Updates is the seed, but public publication still needs policy and permissions.
The deferred work is visible rather than hidden. Every “we’ll do this later” item is captured either as a bead, a callout in the action’s README, or a note in this trail. That makes the deferral honest instead of accidental.
- The Work That Writes Itself — First cairn in this trail. Why Conduit exists at all.
- How Conduit is Shaped — Second cairn. The architectural choices this cairn's operations rest on.
- Timbers — The development ledger Conduit reads from.
- GitHub Composite Actions — The mechanism the publisher uses to share logic across N project repos.
- GitHub Actions permissions/access — The setting that allows other repos in the same org to consume actions from a private repo.
An architecture is a story until somebody onboards a second consumer. It holds up only if the operating experience is small enough to fit on a sticky note. Conduit’s does. Most of the running cost is reading the artifacts, not maintaining the pipeline that produces them.
This cairn is the runbook view: what you’d hand a teammate adding a new project to Conduit, or a future-you who came back after six months and forgot what the moving parts are.
Onboarding a new project
Adding a new project to Conduit is a four-step task. Most of those steps are mechanical.
flowchart LR P[Step 1<br/>project.json] --> W[Step 2<br/>workflow YAMLs] W --> SE[Step 3<br/>secret + AWS role] SE --> A[Step 4<br/>access setting] A --> D[Done. Push to main.]
The four steps:
Step 1: .conduit/project.json. The project’s identity card. Five fields: name, description, color, icon, repo_url. The consumer site reads this to render the project’s landing page, recent-activity feed, and color accents.
Step 2: workflow YAMLs for the channels you want. Each workflow checks out full history, runs jdx/mise-action, configures AWS credentials, and calls uses: Constructured/conduit/.github/actions/publish@v4. The artifact_types input is explicit. Current valid types are pulse, devblog, adr, product-updates, milestone, and brief; the old release-notes name now fails loudly and points operators to product-updates.
Step 3: one repo secret and one repo variable. CONDUIT_DISPATCH_TOKEN is a GitHub token that can dispatch into the Conduit consumer repo and read its existing ADR files, which is how the publisher derives the next ADR number. AWS_CICD_ROLE_ARN is a repo variable naming the IAM role assumed by aws-actions/configure-aws-credentials; that role needs bedrock:InvokeModel on the selected model or inference profile. There is no LLM API key anymore.
Step 4: the access setting on the action’s host repo. Conduit is a private repo. For another repo in the same GitHub organization to consume actions from it, the action’s host repo needs actions/permissions/access set to organization. This is a one-time setting per host-org pair (already done for Constructured), but it is invisible in workflow YAML and easy to forget when standing up a new org.
If onboarding fails with Unable to resolve action ‘constructured/conduit’, repository not found, that’s almost certainly the access setting. Run gh api -X PUT repos/<owner>/<action-host-repo>/actions/permissions/access -f access_level=organization to fix it.
That’s the entire onboarding. Total time: ~15 minutes if you are copying from an existing consumer, plus whatever it takes to provision the AWS role and token through whatever credential management your team uses. Existing .conduit/state.json files can sit there as historical baggage, but the v4 action does not read or write them.
What happens on a workflow run
From a single trigger to a published artifact, the run goes through a fixed series of steps. Knowing this sequence makes the failure modes legible.
sequenceDiagram
participant Trigger as Trigger<br/>(push or cron)
participant Workflow as Workflow YAML
participant Action as Composite Action
participant Timbers as timbers CLI
participant Bedrock as AWS Bedrock
participant Conduit as Conduit repo
Trigger->>Workflow: fire
Workflow->>Action: uses: ...@v4
Action->>Action: Install timbers
Action->>Action: Install canonical templates
Action->>Bedrock: Probe model via invoke-llm.sh
Action->>Action: Resolve per-type time windows
Action->>Timbers: query --range
Action->>Timbers: draft <template>
Timbers->>Bedrock: converse
Bedrock-->>Timbers: artifact body
Action->>Action: Wrap with frontmatter, build bundle
alt artifact_count > 0
Action->>Conduit: dispatch event_type=project-artifact-bundle
Action->>Workflow: no caller-repo commit
else quiet window
Action->>Workflow: silent (no commit)
end
Action->>Workflow: surface op_failures (red if > 0)
The two paths at the end are the important property. A run with at least one artifact dispatches a bundle to Conduit. A run with zero artifacts and zero operational failures stays silent — no commit, no notice. A run with operational failures fails the workflow loudly, after dispatch succeeds for whichever types did work, so partial-success artifacts still land and the failure is visible alongside.
The probe step deserves its own paragraph. The action sends a tiny prompt through invoke-llm.sh, using the same Bedrock Converse path that real drafts use. Healthy runs log the model reply; unhealthy runs emit a GitHub warning with the first lines of the Bedrock/AWS CLI error. This is observability for the credential and model path, which are otherwise easy to misread as a quiet week. The probe never fails the workflow on its own; it is informational. But a downstream draft failure now ships with the actual upstream context attached.
Versioning policy
The action ships at @v4, and that tag is deliberately a moving reference rather than a frozen one. Patch fixes — bug fixes, prompt tweaks, observability additions — re-tag v4 in place. Consumers pinned to @v4 pick the change up on their next workflow run, with no per-repo PR.
A future breaking change to the bundle envelope or the input contract would ship as @v5. The already-completed major bumps are the model: each consumer updates its uses: pin when ready, with the older major continuing to work in the meantime.
The versioning matrix in plain English:
| Change | Tag motion | Consumer cost |
|---|---|---|
| Bug fix in publisher script | retag v4 in place |
none — picked up on next run |
| Template improvement | retag v4 in place |
none — picked up on next run |
| New optional input | retag v4 in place |
none — existing callers ignore the new input |
| Required input change | bump to v5 |
each consumer updates pin at its own pace |
| Bundle envelope schema break | bump to v5 |
each consumer updates pin at its own pace |
| Output shape change | bump to v5 |
each consumer updates pin at its own pace |
Re-tagging v4 is a force-push to the tag. Anyone who fetched the old v4 locally has a stale cache; CI consumers re-resolve the tag at workflow runtime so they pick up the new commit transparently. The blast radius is “humans with stale local clones” plus possibly some CI caching layers, but never CI execution itself. We have done it twice without trouble.
For consumers that want stricter immutability, pinning to a specific commit SHA (@<40-char-sha>) instead of @v4 is supported and documented. We did this during the first migration windows before the floating major tag was stable. The trade-off is “you lose patch fixes until you bump the SHA,” which is sometimes the right answer.
Operational failures and how to read them
Three failure modes have shown up in production, and each looks distinct in the workflow log:
The AWS role or model path is bad. The probe step warns with the Bedrock/AWS CLI error inline. The draft step then fails through invoke-llm.sh, the script records op_failures > 0, and the final step fails the workflow red. The remediation is project-side: fix the repo variable, OIDC trust relationship, aws_region, or model input.
The role is valid but the model is not accessible. Bedrock rejects the configured model ID or inference profile. The action default is global.anthropic.claude-opus-4-7; the caller’s role needs bedrock:InvokeModel on that profile ARN (or on whichever model you override to). Cross-region inference profiles still require an AWS signing region, so aws_region remains an input even when the model routes globally.
The repository_dispatch is rejected. This is rare and usually means the CONDUIT_DISPATCH_TOKEN lacks the right scope, or the consumer repo’s webhook is misconfigured. The dispatch step fails with a 422 or 404 from gh api. The most common subform is the client_payload is not an object 422, which was a bug we fixed by switching from gh api -F to jq | gh api --input -. Now fixed in the action; if you see it on your own integration, you have copied an old recipe.
“Loud failure” in this system means: workflow goes red, the upstream error body is in the log, and the artifacts that did succeed still ship. There is no swallowed exception, no silent skip on operational error, no “partial run pretends to be a quiet week.”
Recovering from any of these is a project-side action. The Conduit-side cost is roughly zero in normal failure modes — the consumer site doesn’t care that a specific run failed, because it only sees what was successfully dispatched.
Backfill and replay
Two scenarios need a way to override the natural time-window flow:
Backfill after a long quiet period. A repo went silent for a month, then resumed activity. The default windows are intentionally small: pulse looks back 24 hours, while devblog, adr, product-updates, brief, and milestone default to seven days. If a project needs a longer or shorter window, set the per-type <TYPE>_WINDOW_HOURS environment variable.
Re-running against a specific window. Maybe a previous run dispatched a bad artifact, you’ve fixed the prompt, and you want to re-draft the same range. The decisions workflow exposes since_sha_override, which forces every requested type to use that SHA as the range start instead of the time-window start:
gh workflow run publish-conduit-decisions.yml \
--ref main \
--field since_sha_override=<sha> \
--field artifact_types=adr,brief
The action validates since_sha_override up front: it must be reachable from HEAD. A typo or truncated SHA fails loud rather than falling silently back to the default window.
The override applies to every requested type for the duration of that run, and does not write any permanent caller-side state. Treat since_sha_override as a transient backfill knob, not a configuration knob.
This is the same mechanism that makes migrating from a different publisher cleanly. If a project had a previous (non-Conduit) workflow that tracked a last_published_sha, you can run a one-off since_sha_override dispatch covering the gap window if you want backfill.
Templates as a shared resource
The canonical timbers prompts ship inside the action and get installed into the runner’s ~/.config/timbers/templates/ on every run. timbers’ resolution order is project → global → built-in, which means:
- A project that has no
.timbers/templates/of its own (the default state) gets the canonical Conduit prompts. - A project that wants to override a single template (say,
devblog.md) can drop one file in its own.timbers/templates/. Only that file overrides; the other templates still come from the action. - A project’s overrides do not affect other consumers — each runner is isolated to its own project repo’s checkout.
This is the cleanest split we found. Every consumer gets the same default voice. A project that legitimately needs to differ (different tone, different audience) can override one template at a time without forking the whole publisher.
Editing a prompt becomes a one-PR change against the conduit repo. Every consumer picks it up on the next run after the v4 retag. That property is what makes prompt iteration manageable across multiple projects without going insane.
timbers-templates/decision-log.md, get it reviewed, merge, retag v4. Strike picks it up on the next push; vantage on the next Sunday cron. No project-side PRs needed unless somebody wants to override.The cost of changing the system’s editorial voice is “review one prompt PR.” That is a property worth protecting.
What’s deferred until the system grows
Several things are deliberately not built yet, with the trade-offs visible.
Q reviewer support across all repos. The team’s automated Claude Code reviewer (Q) currently reviews PRs in osprey-strike but not in conduit, vantage, or cairns. Until that ships, the PR-review workflow rule we use (“re-request review after fix, wait for re-review, converge”) is one-sided in those repos. The rule is in place; the loop will close when the reviewer matures. Tracked in beads.
Multi-cycle convergence loop. The longer-term goal is for the reviewer to participate in a structured back-and-forth: the implementer pushes fixes, the reviewer looks again, and a circuit breaker stops the loop after a configurable number of exchanges (default 3) if no convergence is reached. The mechanics on the implementer side are written into our project rules; the mechanics on the reviewer side are pending. Tracked in beads.
Release-notes drafting. release-notes is no longer the artifact type; customer-facing change summaries are product-updates. What is still missing is a separate tag-push release lane that can draft from release boundaries rather than from the weekly product-update window.
Migration from option 2 to option 3. The current architecture (publisher in the consumer repo, executed in each project repo’s CI) is the right fit for our scale. If the consumer count grows past five-or-so, the conduit-side puller (option 3) becomes worth considering. The migration is real and not a one-way door — the publisher logic moves into a conduit-side script, project-side workflows get deleted, and scope/state moves to conduit-side files. We do not need this yet.
Customer-facing slice. Conduit publishes for an internal audience. A future customer-facing slice (seeded by Product Updates plus selected Recaps) would require a permission model on the consumer side and a different bundle-envelope tag indicating “this is for the public feed.” Not built; not blocked; will be built when there is a real customer-facing need.
The deferred work is visible rather than hidden. Every “we’ll do this later” item is captured either as a bead, a callout in the action’s README, or a note in this trail. That makes the deferral honest instead of accidental.
Summary
- Onboarding is four mechanical steps. One project JSON file, the workflow YAMLs for the channels you want, one dispatch secret, one AWS role variable, one access setting. Roughly 20 minutes for the second project after the first, assuming the IAM role already exists.
- The action's run sequence has three terminal states. Green-with-artifact, green-silent, red. Each is visually distinct in the workflow UI; lumping them was a real bug.
- Versioning is asymmetric. Patch fixes retag
v4in place; breaking changes ship asv5. Consumers pin to@v4for floating patches or to a SHA for strict immutability. - Failure modes are observable. The Bedrock probe surfaces credential and model-access issues before the draft step runs;
op_failuresdistinguishes operational failure from quiet weeks. - Backfill and replay are first-class. The
since_sha_overrideworkflow_dispatch input lets you re-run against any reachable commit, with up-front validation that fails loud on a typo. - Templates are a shared resource. Canonical prompts ship in the action; project overrides are file-by-file. Editing voice across all consumers is one PR.
- Deferred work is visible. What's not built yet — Q reviewer for non-Strike repos, multi-cycle convergence, tag-driven release automation, conduit-side puller, customer-facing slice — is captured in beads or in the action's README, not assumed.
- The probe currently runs on every workflow invocation, which is one extra Bedrock call per run. Is that the right cost-benefit ratio, or should the probe be opt-in (and miss a class of failures it currently catches)?
- The five-step onboarding still requires per-project GitHub and AWS setup. Would an org-level dispatch secret plus a shared OIDC role be a meaningful simplification, or would it introduce more risk than it removes?
- If we wanted to drop the cron schedule for the decisions workflow and trigger it instead on "tag push" or "PR-merge with a label," what would change about the cadence semantics? Would the LLM still get a wide-enough window to consolidate iterations, or would we be back to the per-PR problem?
- The Work That Writes Itself — First cairn in this trail. Why Conduit exists at all.
- How Conduit is Shaped — Second cairn in this trail. The architectural choices that this cairn's operations rest on.
- Timbers — The development ledger Conduit reads from. Required reading if you want to understand what shapes the artifacts the publisher produces.
- GitHub Composite Actions — The mechanism the publisher uses to share logic across N project repos. The action's
action.ymlandbuild-bundle.shin the conduit repo are the canonical references. - GitHub Actions permissions/access endpoint — The setting that allows other repos in the same org to consume actions from a private repo. Required configuration for cross-repo composite actions; easy to forget.
- Amazon Bedrock Models — AWS's supported-model reference. Useful when choosing a Bedrock model ID or cross-region inference profile for the publisher.
- Cloudflare Pages — The hosting layer for the Conduit static site. Build-on-push deployment; no runtime configuration to manage.
Generated by Cairns · Agent-powered with Claude