The Spektacular (Spek) stage runner is the Hive side of long-running runs
(#8303, part of the #8290 umbrella). A run moves through the
spec, plan, and implement stages on one task lease. Spek owns the
state of each artifact; Hive owns the workflow: it asks Spek whether the
current artifact is finished and advances the lease when it is.
The runner is OFF by default. With runs.spektacular.enabled unset or false
Hive never starts a Spek process and every existing lease behaviour is
unchanged.
Every retained Spek run is also projected as a dashboard campaign under
GET /api/campaigns. The campaign id is the stable run/spec key (the same bare
artifact name the status verbs use), the engine is Spektacular, and the row
links back to /api/runs/{key} when run detail is available. Picking up a
Spek campaign does not copy spek contents into Hive; POST /api/campaigns/{id}/resume returns the retained run projection and a CLI status
command, while Spek reloads the current state from its working files and
spek backend.
Each campaign also has a Jam workspace for team interaction around the spec.
The dashboard Campaigns list exposes a Jam action, backed by
/api/campaigns/{id}/jam and the /threads, /suggestions, and /polls
subroutes. Phase 1 is async: participants with read-write access can anchor
threads and suggestions to spec sections, vote in advisory polls, and resume
the workspace after a restart. Maintainers record poll decisions with a
rationale; accepted suggestions and decisions create attributed spec revisions
with human or agent/model attribution and diffs.
Live Jam mode upgrades /api/campaigns/{id}/jam/ws to a WebSocket so signed-in
participants can see who is present and which section each person is editing.
Live spec edits carry the sender's last seen spec revision; stale edits are
rejected with a conflict message instead of overwriting newer work, so
reconnecting clients can refresh and merge intentionally.
Maintainers can opt a campaign into GitHub Projects sync from the Jam tab. The sync publishes the current spec, recorded decisions, and derived suggestion items to the linked project; inbound project status updates are recorded on the campaign without rewriting local Jam decisions. Sync failures are kept with retry guidance so the dashboard shows what to fix before trying again.
Sync posts to https://api.github.com/graphql with the hive's GITHUB_TOKEN.
HIVE_JAM_PROJECT_SYNC_URL overrides the endpoint; an override must use
https (plain http is accepted only for loopback hosts) and never receives
GITHUB_TOKEN. Set HIVE_JAM_PROJECT_SYNC_TOKEN to give a custom endpoint its
own bearer token.
Maintainers can invite Spektacular or another configured hive agent into a Jam thread. Agent participation is deliberately review-gated: the attributed agent reply is posted in-thread and any proposed spec text is created as an open suggestion, but a permitted human maintainer must still accept it before the spec revision changes.
runs:
max_stage_retries: 2 # default; generations one stage may burn
spektacular:
enabled: true # default false
binary: spektacular # default; resolved through PATH
poll_interval_s: 30 # defaultThe Governor dialog's Features panel exposes the toggle and the binary path
under "Long-running runs"; the poll interval and the retry budget are yaml
only. The same keys are accepted by PUT /api/config/governor/features as
spektacularEnabled and spektacularBinary. The runner is installed at boot
(cmd/hive, wireSpektacularRunner), so a change to the toggle takes effect
on the next boot, like the other feature switches.
Package layout: pkg/spektacular owns the CLI boundary, the poll loop, and
the receipt; pkg/dashboard exposes its lease registry as a primitives-only
surface on *Server (VisitActiveStageLeases, AdvanceStageLease,
RetryStageLease, RefuseStageLease, EscalateStageLease, ImportRunPlan)
and drives whatever StageRunner was installed with SetStageRunner from the
contribute hub's cleanup tick. The dashboard never imports pkg/spektacular
(its internal-import ratchet); pkg/spektacular.NewHubRunner takes the
server through the LeaseRegistry interface.
The runs triage pass is separate from the Spek runner and is also OFF by
default. When runs.triage.enabled: true, the scheduler classifies each
incoming actionable GitHub issue before a direct-fix kick:
run/specalways admits aspecrun.run/fixalways leaves the issue on the normal direct-fix path.- Complex issues, or issues carrying
spec_labels(defaultkind/feature,Epic,architecture discussion), admit aspecrun by creating the first stage lease. The lease is keyed as the run-stage work source expects: the repository plus the run key and:specsuffix (see work-sources.md). - Simple issues, medium issues, or issues carrying
fix_labels(defaultkind/bug,good first issue) stay on the existing direct-fix path. - Short bodies (
min_body_chars, default 80), unchosen option lists, and unsubstituted template placeholders get one marker comment (hive-triage) asking the reporter for the missing details and are skipped for that cycle.
The decision is stored on the stage lease as triage_verdict and
triage_rationale, surfaced by GET /api/runs and run detail, and copied into
the first stage receipt timeline event. Owner reset with reason triage_fix
retires a run in the spec stage so the issue can return to the direct-fix path.
runs:
triage:
enabled: false # default
spec_labels: [kind/feature, Epic, architecture discussion]
fix_labels: [kind/bug, good first issue]
min_body_chars: 80
clarify_comment: trueruns.triage.clarity_gate (#13577)
pushes back on vague issues before an agent claims them. It is OFF by default
and independent of runs.triage.enabled. When on, the scheduler scores every
human-filed, unclaimed GitHub issue in the kick list, without a model call:
| Signal | Points | Found when the body has |
|---|---|---|
| Goal | 25 | a goal/problem/summary heading, or a goal sentence (should, expected, so that, fails, …) |
| Acceptance criteria | 35 | an Acceptance criteria / Definition of done / Expected behavior heading or line, or a - [ ] checklist |
| Scope | 20 | a code span, a path or file name, or a scope word (component, command, page, out of scope, …) |
| Detail | 20 | 300+ characters (10 at min_body_chars or more) |
| Open choice | −30 | an unchosen high/medium/low list, an unfilled <placeholder>, TBD, ???, not sure if, or maybe |
An issue scoring under threshold (default 60, Settings → Features →
Incoming issue triage → Clarity threshold) is withheld from the kick. Hive
posts one comment carrying <!-- hive:clarity-gate --> with one short question
per missing signal and adds the needs-spec hard-suppress label (the first
label of project.issue_filter.hard_suppress_labels.needs_spec). The issue then
derives stage/045-needs-decision ("parked on a person: needs-spec"), leaves
enumeration and is refused by the claim gate. The marker is never posted twice.
If the comment posts but the label write fails, the issue stays withheld and the
label is retried on the next kick; a restart in between does not lose the
retry, because the gate reads the issue's label history and tells a park that
never landed from one a person removed.
The issue resumes on its own once somebody answers
(#13588). While the gate is
on, a resume sweep (every 5 minutes, at most 10 issues per pass) looks at open
issues carrying needs-spec and lifts it when all of these hold:
- the issue has a
<!-- hive:clarity-gate -->comment written by one of the hive's own accounts (a pasted marker does not count); - the newest
needs-speclabel event is by the hive, so aneeds-speca person added, with or without an older gate comment, is never lifted; - after both, the issue author, or a person other than the hive with write, maintain or admin permission on the repo (checked live), commented.
The sweep removes needs-spec and records the un-park on the stage engine, so
the issue re-derives stage/010-actionable and re-enters the queue. Removing
needs-spec by hand resumes it too. Either way the gate sees the marker and
does not ask again, however the issue scores. Issues filed by Hive or an agent,
issues already claimed and issues carrying approved-direction are not scored.
runs:
triage:
clarity_gate:
enabled: false # default
threshold: 60 # 0-100; 0 uses the defaultruns.triage.prior_art (#13575)
answers "does this already exist?" before any agent claims a feature request.
It is ON by default and independent of runs.triage.enabled. For every
unclaimed GitHub issue of a gated kind (kinds, default feature, rfc,
epic, enhancement — matched as a bare or kind/-prefixed label, or as a
leading [RFC] / RFC: / Epic: title tag; anything labelled bug is never
gated) the scheduler derives the issue's key terms (backticked identifiers,
config keys and --flags weigh 4, title words 3, the most frequent body
words 1) and asks, without a model call:
| Source | What it searches | Hit kind |
|---|---|---|
| knowledge | the hive's knowledge layers (SearchAll, deprecated/superseded facts dropped) |
knowledge |
| settings | the dashboard Settings search index, as the rfc_existing_check does |
setting |
| merged_prs | merged PRs in the repo from the last 12 months | merged PR |
| closed_issues | closed issues in the repo (declined not_planned ones skipped unless duplicate) |
closed issue |
| docs | README*, CHANGELOG*, docs/ in every project.repos checkout under checkouts_dir, by identifier and top terms (local pkg/codeindex snapshot) |
doc |
| code | source files in every registered checkout, by identifier only (local pkg/codeindex snapshot) |
code / setting |
Each candidate's confidence is the share of the issue's weighted terms it
contains; a candidate needs two shared terms including a title term or an
identifier to count at all. Every source is optional and soft (a failed
search drops its candidates only), a missing, dirty or partly indexed
checkout is called out as "checkout missing/partial for " instead of
being read as no prior art — in the gate comment when one is posted, and as a
prior-art-gate-coverage-gap audit entry otherwise; reports with a
still-building index are not cached, the whole search is bounded (20s, one
query per source) and the report is cached per issue content hash, so an
unchanged issue costs nothing on later cycles.
When the best finding reaches threshold (default 70: deliberately
high because a park costs a person a round trip — 70 means the finding
covers about three quarters of the issue's weighted vocabulary including a
title term) what happens depends on who filed the issue:
| Author | Detected by | What the gate does |
|---|---|---|
| a person | author/human, or the enumerator's human verdict |
one <!-- hive:prior-art-gate --> comment quoting up to max_findings (default 5) findings with links and a one-line quote, the stage/017-possible-existing park label, and a discoverability: <feature> was asked for though it exists advisory bead so the docs can be improved. Never auto-closed. |
| the hive's own agent | author/hive, the App login, or an agent/* / origin/* label |
never parked — a <!-- hive:prior-art-findings --> note is attached once and the issue carries on; the claiming agent must open its claim comment with a Gap vs existing paragraph. At filing time the hive-open-issue relay enforces it for feature kinds: a high-confidence match is refused naming the existing item and recorded as a knowledge bead; a medium one is refused unless the body has a Gap vs existing section, and is then filed with the findings note attached. |
| a clanker (another hive, a contributor CLI, an external bot) | author/clanker, or a [bot] / non-human, non-hive login |
parked like a person, with notice that silence closes it: after bot_close_ttl (default 72h) with no reply the resume sweep closes it not_planned with the already-exists label. Reopenable. |
While an issue carries stage/017-possible-existing no agent claims it
(it is a hard suppress, like 045). The resume sweep (every un-park cadence)
answers the author's or a maintainer's reply:
/hive decision proceed — <what is missing>lifts the label, appends a Scope vs existing section with the delta to the issue body as acceptance criteria, and records the un-park so the issue re-derivesstage/010-actionable;/hive decision existsaddsalready-exists, closes the issuenot_plannedand replies with a pointer at the findings;- a plain reply lifts the label too (the claiming agent is told to state the gap first).
The gate's marker keeps it from asking twice; a stage/017 a person added by
hand is never lifted by the sweep. At most max_parks_per_cycle (default 5)
issues are parked per enumeration cycle, so turning the gate on against a
backlog never floods a repo.
runs:
triage:
prior_art:
enabled: true # default
threshold: 70 # 0-100; 0 uses the default
max_findings: 5
kinds: [feature, rfc, epic, enhancement]
max_parks_per_cycle: 5
bot_close_ttl: 72h # clanker parks only; negative disables auto-closeEvery 30 seconds (the contribute hub's cleanup tick) the runner looks at every
lease that carries a stage. For each spec or plan stage whose poll interval
has elapsed it runs
spektacular <spec|plan> status <name>
where <name> is the bare artifact name (000057_git-commit, never
000057_git-commit.md or 000057_git-commit/plan.md): the lease's canonical
work-item key <repo>!<runKey>:<stage> (see work-sources.md)
with the repository prefix and stage suffix stripped, then reduced to the bare
name (spektacular.ArtifactKey; the dashboard applies the same rule in
runKeyOfLease). The CLI has no --json flag (its only global flag is
--fields); every verb already prints JSON, and an unknown flag is a usage
error. Then:
document_status: draftleaves the lease alone.document_status: finalwrites a stage receipt (/data/runs/receipts/<runKey>/<stage>-gen<gen>.json, thestage-receipt/v1shape frompkg/outputschema), records astage_receipttimeline event carrying the receipt digest and path, and calls the sameadvanceLeaseStagepath the API uses. That fires thestage_completedhook and CEL trigger and records thelease_stage_advancedaudit entry exactly as a manual advance would.document_status: stalemeans Spek strict mode invalidated the plan after the spek changed. Hive refuses the plan-to-implement advance, parks the run withwaiting_on=humanandwaiting_reason=stale_plan, and records ablockedtimeline event. The lease is not retried; recovery is a fresh plan/re-approval.- The status payload's
artifact_id, when present, is the durable Spek artifact join key Hive stores in receipts and stage attributes. The barenameremains the CLI address and backward-compatible display alias; older CLIs that lackartifact_idfall back toname. - Progress is decided by
document_status,current_stepandcompleted_stepsonly. The status document'supdated_atis never read for a progress or staleness decision: it is workflow activity only while Spek's in-progress state matches the artifact, and otherwise a file mtime that agit checkout, a reformat or atouchmoves without anything having happened. Spek may also omit it entirely when no workflow state matches, so the parser treats an absent,nullor emptyupdated_at(and an emptycreated_at/closed_at) as unknown. A stale stage is decided by Hive's own lease clock (the lease's expiry), never by the artifact's timestamps.TestUpdatedAtNeverDecidesinpkg/spektacularkeeps it that way. - A lease that lapses without
finalis retried throughretryLeaseStage(a new generation of the same stage,lease_stage_retriedin the audit log) while the budget allows.max_stage_retriescounts generations including the first: at the default of 2 the stage runs once, is retried once, and the second expiry raises an escalation withdecisionseverity (lease_stage_escalatedin the audit log, ablockedtimeline event withseverity=decision). No third generation is ever minted; a person resets the stage or abandons the run. - The
implementstage has no Spek document. The runner never polls it; its completion is the existing hold-gated PR flow.
Ticks are idempotent per lease generation: once a generation has advanced it is never polled or advanced again, a generation change (a retry, or the successor stage of an advance) keeps the poll pacing instead of earning an extra status call, and a second tick at the same instant is a no-op.
A retry is the one lease mutation allowed on an expired lease: it exists because the generation lapsed, so the expired lease is its expected input and receives a fresh window under its new generation. An advance still requires a live lease.
When a plan reaches final the runner runs
spektacular plan export <name> --format json
(requested upstream in
spektacular#50) and
admits the returned tasks through planning.DecomposeFromOutput with
AutoApprove: false. Spek's structure is rendered verbatim into the
planner's task-list shape ([T1] title (depends: T2) [agent_suitable]); no
model is asked to redecompose an already-structured plan.
Until that export verb exists, Hive falls back only when export is unavailable
(for example unknown_subcommand for plan export). The fallback reads the
plan artifact through Spek's existing store boundary:
spektacular plan file read <name>/tasks.json
spektacular plan file read <name>/plan.md
The preferred fallback file is <name>/tasks.json, using the same shape Hive
asked upstream to standardize:
{
"kind": "plan",
"name": "000057_git-commit",
"tasks": [
{
"id": "T1",
"title": "Add encoding helpers",
"repo": "hivecommons/hive",
"depends_on": ["T0"],
"execution": "agent_suitable"
}
]
}If tasks.json is absent, Hive parses a deliberately small plan.md task-list
convention from the same plan directory:
- [T1] Add encoding helpers (repo: hivecommons/hive) [agent_suitable]
- [T2] Wire helpers into the parser (depends: T1)repo, depends, and the trailing execution marker are optional; a missing
task id is assigned in order (T1, T2, ...). If neither fallback yields a
task list, the final plan stays parked and the runner logs the import error.
The epic is found by its run_key metadata (or created with it, bound to the
run key as its external ref). Its plan is a DRAFT: the run-stage work source
lists implement only after ApprovePlan (POST /api/plans/{id}/approve)
sets plan_status to approved. A run whose epic already carries a plan is
not re-imported.
Spek campaigns can opt into a scheduler-owned recheck cadence without changing
the one-shot default. The feature is off unless
runs.spektacular.recheck.enabled: true; opted-in campaigns use
runs.spektacular.recheck.default_interval (default 168h) unless their
retained campaign record carries recheck.interval, and imported delta tasks
are capped by runs.spektacular.recheck.max_delta_tasks (default 50).
When a completed implement campaign is due, or an owner posts
POST /api/campaigns/{id}/recheck, Hive creates a linked Spektacular revision
(revision_of, revision) and starts that revision at the spec stage. The
original campaign is never overwritten; the revision is a new generation with
the prior final spec/plan treated as prior artifacts by convention. Manual
requests return 409 Conflict when another revision is already in flight and
404 Not Found when recheck is disabled unless ?force=true is supplied.
The recheck follows the same human checkpoints as any Spek run. A final spec
still waits at the spec boundary when that checkpoint is enabled, and a final
plan imports as a draft plan until approval. At final-plan import Hive compares
the revision task list with the prior final plan by normalized task-content
hash. Unchanged tasks reuse the prior logical operation identity and are not
imported again; only new or changed tasks become planner children. If the
codebase and prior plan are unchanged, the delta is empty and Hive creates no
new planner tasks or issues. If the delta exceeds max_delta_tasks, Hive
imports the first capped set and records campaign_recheck_delta_capped.
Each revision carries a drift evidence block with codebase_changed,
prior_revision, delta_count, and recheck_reason (cadence or manual).
The codebase signal compares the prior recorded head SHA with the current Git
HEAD when available. Outward discovery can add two optional evidence shapes
before the revision's spec lease starts: typed release/activity/feed signals
as drift.external[], and exact document snapshots as drift.drift_source[].
Both default to empty and perform no discovery network access unless explicitly
configured.
runs:
spektacular:
recheck:
discovery:
enabled: true
timeout: 30s
max_total_items: 100
sources:
- {kind: upstream_release, name: kubernetes, url_or_repo: kubernetes/kubernetes, max_items: 10}
- {kind: repo_activity, name: competitor-runtime, url_or_repo: example/runtime, allow_prerelease: true}
- {kind: standards_feed, name: ietf-http, url_or_repo: https://www.ietf.org/archive/id/atom.xml}
- {kind: landscape, name: project-landscape, max_items: 20}
discovery_proxy: http://relay:18443
egress_allowlist: [api.github.com, www.ietf.org]
sources:
- name: upstream-release-doc
kind: release
url: https://api.github.com/repos/jumppad-labs/spektacular/releases/latest
variables:
security:
http_allowlist: [api.github.com, www.ietf.org]Typed findings are recorded on the revision as
drift.external[] = {source, kind, title, url, published_at, summary} with
summaries capped at 280 characters, plus external_count and
sources_failed[] = {name, reason}. A failed source is non-fatal: Hive records
the typed reason and continues with the rest. The same evidence is included in
the spec stage assignment prompt as context so the spec generator can decide
what, if anything, belongs in the revised spec; the spec and plan human
checkpoints remain the only approval gates.
Exact document sources use runs.spektacular.recheck.sources.
discovery_proxy must be the operator's existing relay egress proxy, with its
normal network policy and trust roots installed. It is required when exact
sources are configured; discovery never falls back to direct access, including
when NO_PROXY matches. egress_allowlist contains exact DNS hostnames, with
no wildcards or IP literals. Config validation rejects a source outside that
list, even when cadence is disabled, because forced manual rechecks use the
same policy.
Each exact source declares a unique name, a kind (release, changelog,
repo, standards, or landscape), and an exact HTTPS url. Source kind
labels provenance; it does not enable crawling or additional API calls. A pass
makes one GET per declared document, at most 16 sources, with a 10-second
request timeout, a 30-second total deadline, and a 32 KiB UTF-8 response cap.
Redirects are refused. No links are followed and no writes are issued. Each
retained snapshot includes its name, kind, URL, SHA-256 and text; failed reads
retain an error instead of claiming no drift. This exact-document path does not
invoke upstreamwatch's multi-page PR and release scanner.
Campaign list/detail JSON includes
recheck: {enabled, interval, last_at, next_at, in_flight, last_delta_count, external_count, sources_failed}; recheck revisions also include the drift
block above. The dashboard Campaigns panel renders the cadence, shows
N external signals with an expandable list, and provides an owner-only
Recheck action.
The #8227 questions were answered on hivecommons/spektacular#45. The runner encodes the answers and still handles the alternatives it cannot rule out:
- The bare artifact name is the join key. The stable key across spek,
plan and implement is the artifact name itself (
000057_git-commit), shared by convention across the spek file, the plan directory and the changelog record, and recorded as the workflow'sdata.name. It is NOT thespec:/plan:frontmatter cross-references the status response also carries: those are almost never populated (0 of 57 speks, 3 of 55 plans in the Spek repository itself), so the runner surfaces them asArtifactStatus.Spec/.Planfor diagnostics only and never joins on them. Every spelling of an address (<name>.md,<name>/plan.md) reduces to the bare name before it reaches the CLI. If a status answer ever carries a different barenamethan the one requested, the runner treats it as a contract violation and does not advance. - Invalidation after
final. Ifdocument_statusgoes fromfinalback todraftunder the same name, the runner treats the artifact as a stale plan: it refuses to advance, logs, and recordslease_stage_refusedwith reasonstale_plan. If the artifact the lease is bound to disappears after having been observed (a new document with a new name replaced it), the runner refuses with reasonreplaced_documentand parks the lease for an explicit reset. It never rebinds a lease to a document it was not minted for. A parked stage is polled again only after its generation changes.
Hive never opens a Spek file. Every fact about an artifact reaches the
runner through the CLI boundary (Runner.Exec), which is also the seam tests
replace. TestNoDirectFileAccess in pkg/spektacular scans the package for
file access to keep it that way.
pkg/spektacular/testdata/spektacular-fake/spektacular is a shell fake of the
CLI. It encodes the per-artifact status contract exactly as
hivecommons/spektacular#45 ships it, after the Spek maintainer's
review of 2026-09-23 answered the questions Hive had left open. The original
assumptions from the first cut of the runner (PR #8398) and their fate:
Confirmed:
spektacular <spec|plan> status <name>prints one JSON object withkind,name,document_status(draft|final),current_step,completed_steps[],created_at,updated_at,closed_at(#8301). The response also carrieserror: false(every Spek result does) and thespec/planfrontmatter cross-references.created_atandclosed_atare frontmatter dates emitted as RFC3339 midnight UTC.- A missing artifact exits non-zero and prints the JSON error envelope on
stdout. The code is
artifact_not_found(thefileverbs usenot_foundfor the same condition; both are typed asNotFoundError). - Returning a
finalartifact todraftflipsdocument_statuson the same document and clears its close date; it does not create a new document. This is stated in the #45 description from the metadata lifecycle rules and was not contradicted in review, though the maintainer did not address it explicitly. Thestale_planrefusal is therefore the normal invalidation path andreplaced_documentthe exception.
Changed:
- The join key is the bare artifact name, not the
spec:/plan:cross-references (see above). Hive now strips a document path and a markdown extension from every address before it reaches the CLI. - The error envelope is
{"error": true, "code", "message", "resource", "next_action"}, not{"error": "<message>", "code"}. The parser accepts both (the string form stays as a defensive fallback). updated_atis "last modified", not "last activity", and may be absent. Hive no longer uses it anywhere: the receipt ends atclosed_at(or the advance instant) and its input hash excludesupdated_atso two observations of one final document hash the same.closed_atis""while the document is open, notnull;created_atmay be""when the frontmatter carries no date. Both decode as unknown.- There is no
--jsonflag on any verb (unknown flag: --json); output is already JSON. Hive passes none.spec file list/plan file listlikewise take no flag; Hive does not call them today, but Spek may add aModTimeper list entry so one list call can replace N status calls, which is the shape a future list-based poll would consume. plan status <name>reportsplan.mdonly (PlanFilePathhardcodes it), so a plan'sdocument_statusis that ofplan.md, not of the whole plan document set. Hive advances on it as the plan's status; if a plan ever grows documents whose completion matters, the verb has to widen first.
Still open:
spektacular plan export <name> --format jsonprinting{kind: "plan", name, tasks: [{id, title, repo, depends_on, execution}]}for a final plan is still an upstream ask (spektacular#50). Until it lands, Hive advances final plans through the documented<name>/tasks.jsonor<name>/plan.mdfallback contract above.
Scenarios: draft-final (draft, draft, final), never-final,
final-then-draft, final-no-updated-at (final with the updated_at
member absent), missing, selected through SPEK_FAKE_SCENARIO. The fake
answers artifact_not_found for a name spelled with an extension or a
document path, as the real store does, and a usage error for --json.
- work-sources.md: run stages as work items
(
run_stages: true) and the<repo>!<runKey>:<stage>key. - hooks.md: the
stage_completedtransition. - design/run-artifacts.md: multi-repo waves.