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.

Tip

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
Warning

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.

Definition

“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.

Tip

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.

Scenario: Updating the decision-log voice across all projects
@Bob "The ADR voice has been too dry. Can we make it lead with a one-line verdict before the context?"
@Q Open a PR on conduit's 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.

Key Takeaway

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

  1. 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.
  2. 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.
  3. Versioning is asymmetric. Patch fixes retag v4 in place; breaking changes ship as v5. Consumers pin to @v4 for floating patches or to a SHA for strict immutability.
  4. Failure modes are observable. The Bedrock probe surfaces credential and model-access issues before the draft step runs; op_failures distinguishes operational failure from quiet weeks.
  5. Backfill and replay are first-class. The since_sha_override workflow_dispatch input lets you re-run against any reachable commit, with up-front validation that fails loud on a typo.
  6. 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.
  7. 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?
  1. The Work That Writes Itself — First cairn in this trail. Why Conduit exists at all.
  2. How Conduit is Shaped — Second cairn in this trail. The architectural choices that this cairn's operations rest on.
  3. Timbers — The development ledger Conduit reads from. Required reading if you want to understand what shapes the artifacts the publisher produces.
  4. GitHub Composite Actions — The mechanism the publisher uses to share logic across N project repos. The action's action.yml and build-bundle.sh in the conduit repo are the canonical references.
  5. 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.
  6. Amazon Bedrock Models — AWS's supported-model reference. Useful when choosing a Bedrock model ID or cross-region inference profile for the publisher.
  7. Cloudflare Pages — The hosting layer for the Conduit static site. Build-on-push deployment; no runtime configuration to manage.