striatum Specification
July 2, 2026 · View on GitHub
Status: implementation contract Date: 2026-05-06
This specification binds the V1 MVP described in
docs/design/V1_MVP_DESIGN.md and synthesized in
docs/reviews/v1/V1_MVP_SYNTHESIS.md.
Product Boundary
striatum is a standalone, local-first workflow runner for
terminal-based AI coding agents. It coordinates registered target
repositories through a local daemon, daemon RPC methods, and
capability-gated client surfaces (CLI, MCP, and local web UI). It
does not provide a hosted control plane, Striatum-managed external
persistence service, bundled cloud service, telemetry, Slack/remote
serving, durable transcript capture/export, provider SDK integration,
malicious-local-operator-resistant sealed apply, or automatic commits.
Blob storage is an operator-configured durability backend, not a Striatum-hosted service. Under RFC 0072, RFC 0123, and RFC 0171, selected artifact exhaust and generated-record bodies may be stored in an operator-provided local or S3-compatible blob endpoint; the daemon records keys and hashes in PostgreSQL, which remains the authoritative live state and index. Striatum neither provisions nor manages that endpoint and does not use it for telemetry, remote transcript export, or workflow authority; see § Artifacts.
RFC 0033, RFC 0043, and RFC 0048 establish the current substrate:
daemon-owned PostgreSQL is authoritative for daemon-global state and
per-repository workflow state; .striatum/ next to a target repository
is operational scratch only. RFC 0030 supplies the daemon RPC envelope,
RFC 0031 supplies daemon-owned supervision/apply foundations, and RFC 0032's
cross-repository workflow surface is retired by D270; its remaining durable
value is historical schema and MCP mutation capability provenance. Hosted
service semantics, bundled PostgreSQL, and Striatum-managed blob hosting
remain separate future product decisions.
The authoritative live state is the daemon-owned PostgreSQL instance
(RFC 0033) under a repository_id scope per registered target
repository. Per D094 / RFC 0043,
this supersedes the V1 carve-out that kept repo-local workflow state
in .striatum/retired-local-state. The daemon is a hard prerequisite for
every Striatum verb; the V1 --no-daemon flag is retired and parsing
it returns the standard argparse "unrecognized arguments" error.
Repository artifacts are durable provenance only. Marker files, tmux
panes, terminal output, and provider hooks are never live
control-plane state. See docs/how-to/postgres-transition.md
for the operator runbook.
RFC 0048 (v1.49.0 → v1.55.0) completed the PostgreSQL substrate port:
every single-repo mutation, recovery, and read handler had a native
PostgreSQL implementation before the Go cutover. D107 / RFC 0068 and D111
set the current target architecture: the production daemon is Go; the Python
daemon module and selector are removed; and the Python MCP wrapper is removed.
RFC 0078 removes the Python runtime, source tree, CLI/web surfaces, and tests
from the active implementation.
The legacy local-state package, root DB/migration facades, direct corpus
exporter, V1 local-state schema module, deterministic repo-local fixture,
and broad skipped compatibility tests are deleted. Remaining
legacy SQLite handling is completely retired, and registrations reject any old SQLite files.
STRIATUM_DAEMON_REQUIRED=0 STRIATUM_TEST_HARNESS=1 escape no
longer takes effect for ported methods — mapped CLI verbs fail
closed instead of falling back to any local database when the daemon is
unreachable or the target repository is not registered. Schema v6
(migration 0006) anchors the per-event hash chain in dedicated
previous_hash / row_hash columns plus a
striatumd.repo_event_chain_heads pointer for O(1) chain-head
reads.
External memory or retrieval systems (Engram, under RFC 0044, is the first
reference consumer) may ingest the read-only striatum corpus export bundle
as optional local augmentation. The runner does not import any such consumer,
does not register memory.* capabilities, and does not call retrieval
during state transitions; see § Corpus Export And Augmentation Boundary.
State Store
striatum repo add <path> [--init] registers a target repository
with the daemon. --init creates .striatum/scratch and the
.gitignore entry for a fresh target repository.
The authoritative workflow state lives in the daemon-owned PostgreSQL
instance under a repository_id scope; .striatum/ is operational
scratch for supervised wrapper FIFOs, pidfiles, and transient supervisor
scratch.
The per-repository schema in the daemon DB holds:
repositories(registry; per-repo identity and lifecycle)workflow_snapshotsrunssessionsjobsjob_dependenciesqueue_messagesleaseswork_packetsartifactsverdictsblockerscommand_requestsprocess_executionseventsjob_worktreesprocess_supervisorsprocess_supervisor_pointers
events and artifact records are append-only (UPDATE/DELETE are
revoked from the daemon read-write role). Mutations use short
serializable Postgres transactions and emit structured events.
Schema upgrades are forward-only, daemon-owned, and applied at
daemon startup; striatum doctor reports the on-disk substrate
version. A database whose schema version is higher than the daemon
binary supports is refused; client/daemon version skew refuses with
exit code 10. The pre-D094 repo-local migration implementation is deleted;
ordinary CLI verbs do not apply or import it.
Day-zero setup is guided by striatum repo add --init, striatum skills install, striatum plugin install, daemon install/start/status,
and doctor. These helpers are bootstrap surfaces: they may render
agent-side bundles, register a repository, render a user service,
repair local Postgres grants, or run smoke checks, but they do not
become an alternate workflow-state authority.
AI-operator cold start is guided by striatum operator bootstrap. The command is
a custom CLI-local entrypoint — it is not a generated 1:1 daemon route — but under
RFC 0167 P0 it is the client of the operator.bootstrap RPC: it mints and presents
a session-bound operator token and leases the operator handle. The live
operator-session/handle/token state is daemon-owned, created server-side by the
RPC (the CLI is not itself a new live-state authority). The minted {admin, read}
token is written 0600 to .striatum/scratch/operator-token and consumed by the
launched operator process through STRIATUM_MCP_TOKEN_FILE (resolved at higher
precedence than the static runtime token); the raw token is never embedded in the
packet. Its stable JSON payload uses
schema_version: "striatum.operator_bootstrap.v1" and is also rendered as a
bounded Markdown-ish human summary by default. It composes daemon reads such as
repo.resolve, status, and doctor with local probes for git identity,
VERSION, daemon runtime token path presence, MCP endpoint path presence, and
docs/operator/BRIEF.md freshness. It must separate active frontier state from
historical run history, cap expanded lists, avoid embedding full status,
doctor, session, verdict, or run-history arrays, and return a small honest
degraded packet with recovery commands when the daemon or token is unavailable.
Writable SQLite import windows are closed. The retired migrate-repo-local
and daemon migrate spellings are fully removed. CLI verbs against an unregistered repo refuse with exit code 12
(repo_not_migrated) and point operators to register with repo add --init; CLI verbs without a
reachable daemon refuse with exit code 11 (daemon_unreachable). Neither
refusal opens or creates any local database file.
Daemon→PostgreSQL authentication and the database-enforced write boundary (RFC 0110)
RFC 0110 (accepted D164; authoritative implementation spec at
docs/operator/artifacts/rfc-0110-pg-auth/spec_publication/synthesis/SPEC_PUBLICATION.md)
moves the "artifact/RPC API is the sole write path" invariant out of the daemon
process and into PostgreSQL itself, in phases. This is the binding model; the
phase nomenclature and claim-keying below are normative.
- Authority gate (L1). Protected durable surfaces are written only through
owner-owned
SECURITY DEFINERfunctions that begin withassert_daemon_authority()— a check of a per-instance, RAM-only secret whose digest lives in an owner-only registry the runtime role (striatumd_rw) cannot read. Direct DML on a protected surface isREVOKEd from the runtime role per phase, so a leaked runtime DSN cannot forge artifacts or tamper with the audit chain: it lacks both the table-level privilege and the secret. Two guarantees are separated: G1 invariant integrity (append-only / hash-chain / attempt-scope cannot be violated) and G2 daemon issuance (a write succeeds only if the caller presents the daemon-authority secret). - In-transaction attribution prelude (L3). Every mutating transaction is
opened through a single constructor that, as its first statement and over the
extended protocol (bound parameters, never simple-protocol text), sets the
authority secret plus
striatum.rpc_id/striatum.principal_id/app.session_idlabels so neither secret nor labels appear inpg_stat_activity. These GUCs are attribution labels only, never authority; RLS row-scoping is defense-in-depth under an already-authorized session, never the trust boundary. - Fail-closed, mutation-coupled audit. For a mutating RPC the audit row is
the final write inside the same transaction, so success-without-audit-row
is impossible. Standalone audit appends (reads, denials, transport errors)
convert an append failure into an
audit_append_failederror rather than succeeding silently. The audit chain hash moves in-DB through a versioned length-prefixed canonical form (hash_format_version3) that is byte-identical in Go and PL/pgSQL; the prior Go-only v2 hash remains the permanent reader of pre-cutover rows, and the cutover is a single operator-gated release step. - Phased write closure (claim-keyed). Closure lands in fixed phases, each
reported by the
striatum doctorpg_write_boundaryposture string, and every operator-facing claim is keyed to it: P0audit_only(audit_logDB-enforced) → P1audit_artifacts(+artifacts) → P2full(+events). The claim "the daemon's durable write paths are DB-enforced" is reserved to P2fulland may not be made before the posture string readsfull. - Ephemeral runtime credential (L0) and lane isolation (L2). The runtime
password is owner-bootstrapped and RAM-only, re-rotated each daemon restart, so
a captured DSN string dies at the next restart; where owner==runtime (the
documented live PEER posture) rotation is inert and surfaces as a doctor
posture finding. A dedicated PG-less lane OS user plus a
0700socket directory (0710when the POSIX ACL mask grants lane traversal only) make PostgreSQL unreachable from lanes as the hardened default; the daemon asserts the owner-only socket directory at startup and refuses permissive/custom shared socket directories. The daemon consumesSTRIATUM_LANE_OS_USERfor supervised lane launch and recordsrun_as_usermetadata, but GH #87 closes only when the host has the dedicated PG-less lane OS user, passwordless sudo, PostgreSQL rejection rules,make lane-isolation-check(T-LANE-ISOLATION-NEG) green in hardened-profile CI for both UNIX-socket and loopback TCP probes, and blocking doctor behavior under the secure profile all live. The current secure-profile doctor gate is controlled bySTRIATUM_SECURITY_PG_SOCKET_HARDENED; when enabled,lane_pg_reachableis a blocking doctor problem instead of an advisory warning.
Read confidentiality against a live runtime credential is not fully
claimed by RFC 0110/#164 yet. Two behavior-changing reductions have landed:
owner bundle 0005 (RFC 0113 R1) revokes direct runtime SELECT on
striatumd.clients.token_hash and striatumd.clients.token_salt, and owner
bundle 0006 (RFC 0114 / D173) transfers ownership of striatumd.principals,
striatumd.principal_clients, and striatumd.client_sessions to the owner
role and revokes direct runtime reads (principals, client_sessions: full
deny; principal_clients: principal_id column denied). Gated reads route
through owner-owned SECURITY DEFINER projections guarded by
assert_daemon_authority(), with SQLSTATE-driven fallback while a database
has not had the bundles applied. striatum doctor DERIVES the pg_read_scope
posture from authority stamps plus live privilege/ownership probes: with
bundles 0005+0006 applied and verified it reports partial_projection_gated
(four gates: clients columns, principals table, principal_clients
columns, client_sessions table), otherwise broad_runtime_select, with a
grant_drift array naming any re-opened surface. private_read_denial
remains false: the 2026-07-01 architecture-review remediation campaign's
first least-privilege slice formalizes clients as
runtime_column_scoped_select, meaning SELECT * and token-secret column reads
are denied while named non-secret metadata columns stay directly selectable and
secret reads route through the authority projections. Prose/workflow tables (RFC
0113 R2), artifact/event metadata (R3), and client_capabilities (RFC 0114
OQ1) are still directly selectable, so L0 rotation and L2 lane isolation still
bound that remaining exposure. The read surface is inventoried in
go/pkg/db/read_authority_inventory.go and guarded against unclassified table
growth. #164 stays open for the remaining surfaces.
The decision log records each per-phase decision on landing.
Operator identity and run attribution (RFC 0167 P0 / D260, D263)
Operator identity is not a new identity system: the operator-id IS a
principals.principal_id of kind human. Owner bundle 0022 adds a
daemon-leased, memorable rendering layer over it — striatumd.operator_handles
(handle#suffix, unique among LIVE handles per repository) and
striatumd.operator_sessions (the pre-run operator-session lifecycle) — plus a
write-once run-origin stamp: runs.created_by_principal_id (the origin
principal, resolved server-side from the live token through the
resolve_principal_for_client projection, never a client-supplied param) and
runs.created_by_handle_id (a per-session handle snapshot). Write-once is
enforced at the database by the runs_origin_write_once BEFORE UPDATE trigger.
The attribution columns are read-closed at the column layer (the C2" composed-route
closure): striatumd_rw loses SELECT on runs.created_by_principal_id,
operator_handles.principal_id, and operator_sessions.{principal_id, client_id},
so neither the direct nor the composed client_capabilities ⋈ operator_handles ⋈ runs
join can reconstruct client_id → principal_id. Every identity read rides a
daemon-secret-gated SECURITY DEFINER projection — run_origin_identity (the
striatum whose <run-id> reverse lookup), runs_for_origin_client (status --mine), and runs_missing_origin (the advisory doctor attribution_unknown
rule). The handle pool is curated, lowercase, privacy-safe; the default is a hash
of principal_id (reconnect-stable, never tty/pane/title/env), escalating to the
next candidate on a live collision. The operator-bootstrap RPC mints a
session-bound operator token ({admin, read}, repo-scoped, TTL-bounded,
close-revoked) and leases the handle in one transaction. striatum operator bootstrap is the client of that RPC: it calls operator.bootstrap and presents
the minted session-bound operator token to the launched operator process / MCP
client (written to .striatum/scratch/operator-token, consumed through
STRIATUM_MCP_TOKEN_FILE, which the agent loop resolves at higher precedence
than the static runtime token). Per the honest blast-radius accounting the static
bootstrap-admin token is used only to call the RPC and remains segregated to the
daemon-root surface — it is not the routine operator repo-admin credential.
Workflow Config
Workflow config is JSON only. The validator rejects .yaml and .yml files
and rejects non-object JSON roots.
Required workflow fields:
schema_versionworkflow_idworkflow_versionnamebranchcoordinatorlanesrolescontext_docsparallelismjobsedgescycles
The V1 schema version is striatum.workflow.v1.
The runner does not infer or select a default workflow for a target
repository. run prepare requires an explicit workflow file path, and
the workflow snapshot for a run is taken from that file. workflow generate is the current scaffold generator; it supports catalog
shapes such as minimal, review, and code_change and previews by
default unless --write is passed. Checked-in examples/ are
fixtures or starting points, not runtime defaults.
RFC 0034 V1 adds a workflow generator over the same schema. RFC 0078 has
ported the active Go generator path to reuse Go workflow validation and lint
before returning output. The legacy Python generator API is retired.
shape: "multi_phase" emits V1.1 with ordered phases and
phase_synthesis jobs. shape: "implementation_panel" emits a V1
workflow from RFC 0074 role/adversary pack options such as
implementation_panel_roles, maintainer_cost, and operator_ergonomics;
it remains a normal workflow tree and does not use RFC 0052 typed
committee artifacts. RFC 0093 collaboration shapes
(falsification_gate, cross_examination) emit V1.1 phased workflows with a
phase_synthesis adjudicator job, a collaboration_ledger artifact, and a
bounded needs_revision cycle back into the dialogue phase. These two generated
shapes are static challenge/rebuttal gates over published artifacts; they do not
keep the author or holder session live and do not imply runtime model-family
attestation for manually registered sessions. shape: "adjudicated_constraint_extraction" emits a V1.1 phased workflow whose
cross-examiner jobs declare explicit interrogation consumers targeting
convener_draft; the shape remains experimental until a later graduation
decision changes the support tier. shape: "divergent_ideation" (RFC 0087 /
RFC 0129) emits a V1 fan-out/fan-in workflow — a frame_problem brief, N
fresh-session diverge branches (one cognitive frame each, round-robined across
the lane ring so a multi-model lane set carries different frames per branch), a
convergence critic that scores/clusters/trap-detects, K deepen jobs, and a final
synthesis. Frames are a curated, distortion-axis-tagged authoring library
selected with an anti-redundancy + min-structure policy; the convergence critic
can run on a different lane/family (the multi-model signal). It adds no daemon
method and no model call in any state transition, and is supported (graduated
D199 per RFC 0106) on a green RFC 0105 fixture covering its double fan-out/join
and branch-death self-recovery. shape: "fog_of_war_review" and shape: "synaptic_prune" (RFC 0094) emit V1.1 phased workflows and are the two deferred
collaboration shapes from RFC 0093. fog_of_war_review distributes disjoint spec
fragments to interrogable reconstructor lanes (a judge alone holds the full spec),
has them reconstruct the hidden constraints through peer interrogation, scores
coverage at a judge-owned collaboration_ledger gate, and withholds the
proposal-typed job behind that verdict via work-packet type sequencing
(gate.withhold_packet_types / until_verdict_clears, compiled to a cross-phase
dependency). synaptic_prune opens a forum with a post_dialog_hook so close
emits a prune fan-out before participant teardown, collects one retire-nomination
per still-live participant, and records every ≥2-vote claim into a
collaboration_ledger as a durable negative preamble. Both remain experimental
until a later RFC 0106 graduation on a green RFC 0105 fixture. Other built-in
shapes emit V1.
Generator preview and workflow validate --json envelopes include the advisory
lint payload, including warning count and coverage summary; lint
remains informational except where a later rule explicitly promotes a finding
to a hard refusal.
The bundled template catalog is package data under go/pkg/workflowtemplates;
V1 does not fetch remote templates and does not load target-repository catalog
extensions. workflow templates list/show expose catalog metadata.
workflow generate writes the generated tree only after the same
immediate validation pass and refuses to overwrite existing generated
files. The local service exposes read endpoints
GET /workflow-templates and GET /workflow-templates/<id>, plus
generation endpoints POST /workflows/generate/preview and
POST /workflows/generate. Preview writes nothing and is not mutation gated;
generation requires the daemon web service to have
STRIATUM_DAEMON_WEB_ALLOW_MUTATIONS=1 at startup. No database migration is
part of RFC 0034 V1.
RFC 0036 V1 adds the chat-assisted scaffolding harness over the same
generator path. The chat closed set includes generate_workflow_preview
at all times and includes generate_workflow_write only when the daemon web
service was started with STRIATUM_DAEMON_WEB_ALLOW_MUTATIONS=1. The write
tool still fails closed unless a separate operator confirmation gesture is
recorded by the web UI.
The validator enforces unique job ids, resolved role/lane references, valid edges, bounded cycles, repo-relative artifact paths, valid shared-resource declarations, and declared parallelism with disjoint write scopes or review-only unique artifact paths.
Cross-repo workflow shape is retired by D270. Current workflow files are
single-repository units: they must not declare top-level repositories,
primary_repository, job-level repository, cross_repo_cycle, or
reviewer_access_scope: "cross_repo_artifact_augmented" as a supported
production shape. Operators who need multi-repository outcomes should run
separate single-repository workflows and coordinate them outside the Striatum
run scheduler. The RFC 0128 write-scope guardrail remains: authoring and
dispatch surfaces fail fast or warn when a workflow attempts to reach outside
the target repository instead of silently narrowing that intent.
workflow validate refuses lint-detected same-model implementer/reviewer
pairings and revision cycles by default; operators can pass
--allow-same-model-pairing to accept that workflow-authoring risk
explicitly. This CLI-level refusal uses the advisory lint rules and does not
change the pure validate_workflow() API or generator preview behavior.
workflow lint also warns when the coordinator/operator lane uses the same
model family as a synthesis, phase-synthesis, collaboration adjudicator, or
final-review content gate. That operator_content_role_model_overlap warning
is advisory; workflows that intentionally accept the risk may set a non-empty
top-level operator_content_neutrality_override_rationale.
Lane selection is workflow-authored. There is no provider-default lane and
lane ids have no built-in semantic meaning. A job with lane_id is queued for
that lane; a job without lane_id is queued without a lane target and can be
claimed by a session with the matching role. register-session records the
session's lane_id, and claim-next matches pending work by run, role,
fresh-session rules, and lane target when one is present.
Lane configs may declare adapter constraints for network access, transcript
handling, and repository scope. The validator accepts only known constraint
names and values, and work packets expose both the requested constraint and the
adapter's recorded enforcement level. Lanes may also declare
required_enforcement for any declared constraint. Validation rejects a lane
when the adapter can only provide a weaker level than the workflow requires.
Workflows may declare review_revision_policy for root review
needs_revision verdicts. V1 supports the explicit
root_review_needs_revision: "human_checkpoint" policy for RFC-style workflows
that intentionally pause for operator judgment instead of entering a revision
loop. Per RFC 0053 the operator is the AI agent by default; the pause routes
to the human principal only when the AI escalates. (The schema field name
human_checkpoint is retained for compatibility; renaming it is deferred.) root_review_needs_revision: "declared_cycle" is accepted only when each
root review job declares a matching needs_revision cycle.
Harness Profiles
Design rationale: RFC 0010.
Workflows may declare an optional harness_profiles map at the top level
and reference one profile per lane via harness_profile_id. The map is a
passthrough projection surfaced to work packets; it does not change
adapter or scheduler behaviour.
V1 validation rules:
tool_familymust be one ofgeneric,codex,claude_code,agy. Other values are rejected.strategy_versionmust be a non-empty string.accountability.native_subagents, when set, must equalinternal_to_parent_session.accountability.first_class_registration, when set, must equalnot_supported.prompt_envelope_path, when set, must be a non-empty repo-relative string with no..segments. Existence is not checked at validate time.fallback_profile_id, when set, must reference a profile declared in the same workflow.- A lane's
harness_profile_id, when set, must reference a profile declared inharness_profiles. Unknown references are rejected. - Unknown sibling fields on a profile body are accepted as lint
warnings, surfaced in
striatum workflow validate --jsonunder thewarningskey. They are not errors in V1; future versions may tighten this. - (V1.5) Repo-relative process-lane command paths that do not exist on
disk surface as lint warnings under the same
warningskey. The check fires whenlane.command[0]looks like a path (contains a slash or starts with.//../) and is missing under the workflow's repo root. Bare binary names (codex,claude,agy) and absolute paths are not checked. The warning is non-blocking; future versions may graduate it to a hard error.
When a job's lane references a declared profile, claim-next adds a
harness_profile block to the work packet:
{
"harness_profile": {
"profile_id": "codex_default",
"tool_family": "codex",
"strategy_version": "2026-05-08",
"...": "every other declared profile field, verbatim"
}
}
Lanes without harness_profile_id produce work packets with no
harness_profile key — the contract for existing workflows is unchanged.
Profiles are referenced at lane level only; job-level overrides are
reserved for a future RFC. The reference fixture lives at
examples/harness-profiles/workflow.json.
Implementation Envelope
When a workflow job has reachable downstream jobs with declared
write_scope or expected_artifacts, claim-next adds
context.implementation_envelope to the work packet. The block lists the
reachable downstream jobs in graph order with their workflow_job_id, type,
role/lane, write scope, and expected artifacts. Its instruction tells design
and synthesis lanes to keep recommended implementation layouts inside those
frozen downstream envelopes, or to call out that the scope must be revised
instead of assuming it can widen later. Workflows with no downstream envelope
produce packets without this key.
Shared Resources
Workflow jobs may declare shared_resources for mutable resources outside the
repository tree, such as a test database, hardware device, or external fixture
that parallel jobs could collide on. Entries may be strings, which are shorthand
for an exclusive resource, or objects:
{
"shared_resources": [
"postgres:test-db",
{
"id": "postgres:test-db",
"mode": "per_lane_namespace",
"namespace": "reviewer_a",
"description": "DB-backed validation fixture"
}
]
}
Object entries require a non-empty id. mode defaults to exclusive and may
be exclusive or per_lane_namespace. Namespace mode requires a non-empty
namespace. The validator rejects malformed declarations. Validation/generator
envelopes and live run graph JSON include declared resources on each planned
job.
workflow validate --json reports parallel_shared_resource_contention when
jobs in the same parallel_group share an exclusive resource id, or when
per_lane_namespace jobs reuse the same namespace. The warning is
informational; V1 does not create daemon-owned scheduler locks for external
resources.
When a claimed job declares shared resources, claim-next adds
context.shared_resources to the work packet. The block lists the current
job's declared resources, includes the parallel_group when present, and names
related parallel jobs with overlapping resource claims. Agents should serialize
mutating validation against exclusive resources or use the declared namespace
before running resource-mutating gates. Workflows with no declared shared
resources produce packets without this key.
Explicit Interrogation Consumers
Workflow jobs may declare interrogation_targets (RFC 0112 V1) to consume the
live preserved context of an upstream interrogable job without adding fake graph
edges:
{
"id": "cross_examiner_1",
"interrogation_targets": [
{"workflow_job_id": "convener_draft", "required": true}
]
}
Each target entry must be an object with a non-empty workflow_job_id; required
is optional and defaults to false. Validation rejects duplicate targets,
self-targeting, unknown target jobs, targets that are not interrogable: true,
targets that are not upstream of the consumer in the workflow graph, and chained
consumers (a job that is itself interrogable: true may not declare
interrogation_targets in V1). Unknown target-entry fields are lint warnings,
not validation errors. workflow validate --json also warns when a job declares
more than three targets, when its lane does not advertise interrogate, or when
a target is already a direct dependency and the declaration is redundant.
The resolver is snapshot-derived: it reads the frozen workflow snapshot and live
jobs rows for the run. Striatum does not materialize interrogation consumers in
a table, does not add synthetic job_dependencies, and does not infer consumers
from terminal output or provider hooks.
An interrogable target's preserved-context window stays open while any direct
dependent or explicit interrogation consumer is non-terminal. Terminal consumer
states for this predicate are completed, failed, canceled, skipped, and
waiting_human. When a consumer enters one of those states, the terminal hook
evaluates the union of direct and explicit consumers and closes the target
session with interrogation_window_closed once none remain pending. Re-opening
the target for a revision closes the prior attempt's target session and any open
interrogations with revision_reopened; the fresh attempt gets its own
session.awaiting_interrogation event. That event payload includes the target's
current workflow_job_id and attempt so packet projection and evidence queries
stay attempt-scoped.
required: true is advisory in V1. It never blocks claiming, completion,
verdicts, or human checkpoints. Instead, terminalizing a consumer that skipped a
required target records interrogation.required_skipped, and an attempted open
against a legitimately retired target records
interrogation.unavailable_signaled. These evidence events are daemon DB facts,
not durable transcript capture.
When a job declares targets, claim-next adds
context.interrogation_targets to its work packet. Each entry includes the
declared workflow_job_id, required, state, reason, and instruction text.
Available targets also include target_session_id and target_attempt.
Unavailable or not-yet-ready targets may include target_attempt and a reason
such as revision_reopened, target_skipped, or target_retired. The
projection is attempt-aware and must not point a consumer at a stale prior
attempt target after a revision re-open. Jobs without declarations produce
packets without the block.
Reviewer Policy
type: "review" jobs may declare two optional policy fields (RFC 0002):
reviewer_access_scopeis one ofdocument_only,artifact_augmented, orrepo_level. It tells the reviewer what they may inspect: only the target documents listed ininputs; those plus supporting artifacts/reports/ledgers also listed ininputs; or the repository within the job's declaredwrite_scope.allowed_paths/forbidden_paths.reviewer_context_policyis one offreshorcross_round.freshrequires a brand-new role/session with no prior thread state;cross_roundlets the reviewer retain context to verify whether previously raised issues were resolved.
Validation rejects unknown values, non-review jobs that declare either field,
and the explicit conflict between reviewer_context_policy: "fresh" and
fresh_session_required: false. When a review job declares
reviewer_context_policy: "fresh" and does not set fresh_session_required,
the prepared job row is silently stored with fresh_session_required = 1.
When a review job declares either field, work packets gain a review_policy
block that exposes access_scope, context_policy, and a deterministic
instruction string. The instruction is the access-scope sentence followed by
a single space and the context-policy sentence, so reviewers can be prompted
without parsing the policy values themselves. Workflows that do not declare
the fields produce work packets without the block, preserving prior behavior.
Review Postures (RFC 0018 V1)
Review jobs may also declare an optional review_posture field that names
the kind of adversarial reading the reviewer is performing. V1 ships nine
first-class postures plus a custom:<name> grammar for off-list flavors:
neutral | devils_advocate | security | threat_model |
latency_performance | ergonomics_dx | accessibility |
compliance_license | supply_chain | custom:<non-empty>
When declared, the work packet's review_policy block includes a posture
key and the instruction string gains a deterministic posture-specific
sentence (e.g. security appends "This is a security-focused review. […]
verdict acceptance means you actively looked and found nothing actionable.").
Custom postures expose the literal string but get no auto-appended
sentence; the workflow author owns the prompt body for off-list flavors.
Build jobs may declare required_review_postures: a non-empty list of
posture names declaring which adversarial coverage the build wants. The
workflow validator walks the directed edge graph in both directions from
each such build and refuses (WorkflowError, exit code 8) when any
required posture is not the review_posture of a reachable review job.
This catches mis-wired posture coverage at workflow-validate / run-prepare
time, before any session claims work. Runtime enforcement is preserved
by the existing edge-verdict gate (a downstream-of-review job stays
blocked until the review accepts) plus run-completion semantics; no
separate runtime gate is added in V1.
Design rationale: RFC 0018; see also
docs/dogfood/016/decisions/V1_ACCEPTANCE.mdfor the lifecycle re-cast (D069).
Reviewer Independence (advisory)
fresh_session_required: true and reviewer_context_policy: fresh are
advisory beyond what the runner can mechanically observe. The runner
enforces session-id distinctness (a reviewer session is a different
session_id from the author session) and refuses to register a fresh
reviewer when an active author session already exists in the run, unless
register-session --force-non-fresh --reason "..." is passed. The reason
is recorded on the session row (sessions.non_fresh_reason) so evidence
exports document the override explicitly.
What the runner cannot verify: whether the OS process driving the
reviewer session has actually been kept free of the author's context.
A single human at a single keyboard can satisfy session-id distinctness
trivially while still having read the entire draft handoff. striatum doctor surfaces two observable breaches as
reviewer_independence_unverified problem records:
- Two active sessions in the same run whose supervisor rows share a
pid. Same OS process is driving both lanes. - An active reviewer session on a run whose author session has an active supervisor but the reviewer does not. The asymmetric supervised/unsupervised mix usually means the operator is driving the reviewer manually from the same shell as the author.
Operator obligation: when running with --force-non-fresh, the recorded
reason should describe how independence was preserved (e.g., "different
agent CLI invoked from a fresh shell", "review delegated to teammate")
or explicitly note the breach ("operator drove both lanes; HARNESS-001
working supervised lane not yet shipped"). The runner records the string
verbatim; reviewers and auditors read it later.
Byline Integrity
Workflow-declared expected_artifacts.author_line is computed at packet
and publish time from the session's current lane-liveness attestation
(RFC 0026). A session is lane-attested only when it has an attached
process_supervisors row for the same run and session, the recorded pid
is alive, the recorded Linux /proc/<pid>/stat start-time token still
matches, and the supervisor command equals the session lane command from
the immutable workflow snapshot. starting supervisors do not attest a
lane. Platforms that cannot provide a stable process-start token are
unattested rather than silently upgraded.
Attestation is not model-token authorship proof and is not source-byte
provenance. It means only that the runner has a live process binding for
the declared lane. Unattested sessions publish under author: operator
or, when registered with --operator-label <label>, under the YAML-safe
form author: operator-self-declared-<label>. Operator labels must match
^[a-z0-9._-]{1,64}$ and may not be reserved attestation terms, lane
ids, or role/model/ordinal-shaped bylines. Older packets may contain the
historical title-block form author: operator [self-declared: <label>];
the publisher canonicalizes that legacy form to the current YAML-safe
line when it appears outside front matter, but new packets do not emit it.
The runner records the actual author: line read from each published
Markdown artifact in artifacts.author_line; when the file omits the
line entirely the column is NULL. Evidence exports and run summaries read
the actual column, so a missing byline renders as author: <missing>
rather than the workflow's expected string. This prevents the snapshot
lying about who reviewed when the operator drove a job whose declared
lane never executed it (HARNESS-003).
Review jobs may declare require_attested_lane: true. In V1 this field
is valid only on review jobs. When set, publish-artifact, verdict,
and submit-review refuse before side effects unless the calling
session is lane-attested, and the error points operators at
striatum supervise start --session-id <id>.
Provenance Modes
Workflows may declare provenance_mode. The closed set is advisory,
attested_bylines, and sealed_patch; absent mode defaults to
advisory.
advisory is the current default provenance level: Striatum records
workflow state, artifacts, verdicts, and evidence, but it does not
prevent an operator with native file tools from editing source bytes
directly.
attested_bylines means RFC 0026 lane-liveness attestation affects
byline derivation and optional review-job gates. It still does not prove
artifact bytes came from a model process and does not prevent direct
source edits.
sealed_patch is the reserved hard-containment mode. The workflow
validator accepts structurally valid sealed_patch workflows with
non-overlapping repo-relative protected_paths and
operator_writable_paths. Daemon-backed run start refuses sealed
runs unless the daemon has the required containment/apply authority.
Daemon-mediated sealed apply is represented by RFC 0031 apply receipts
and fail-closed apply authority. Silent downgrade to advisory is a
correctness bug.
Operator Modes
Workflows may declare operator_mode: constrained as the RFC 0099 Phase 1
operator assertion. The assertion is surfaced in status --run-id and
run.summary so the operator boundary is visible in run reads. In V1 this
field is advisory: it does not by itself sandbox an AI operator. The initial
RFC 0099 mediated surfaces are repo.write, an exact-content mutation,
repo.patch-preview / repo.patch-apply, scoped unified-patch operations that
check applyability and all changed paths before mutation, and process.run, a
command-array execution surface. These surfaces require active job leases;
repository mutation surfaces require repo-write jobs and refuse paths outside
the job's write_scope before writing. process.run requires
capability_requirements.process_execution: true and records
process_executions evidence without durable stdout/stderr transcripts. If the
job lacks that process-execution opt-in, process.run is refused unless a prior
typed escape decision exists for the same run with escape_surface equal to
process.run or shell_command and escape_action equal to either the joined
command text or sha256:<command_sha256>.
Effective write scope is frozen per job attempt. The scope in the work packet,
and the scope used by artifact.publish, repo.write, repo.patch-preview,
repo.patch-apply, process.run, and work.complete, is the attempt's frozen
scope rather than a later re-render from mutable workflow or context files. If a
mid-run context or workflow edit would make the current artifact path, dirty
tree, patch, process output, or completion check depend on a wider effective
scope, the repository-touching surface fails closed with a typed write-scope
error that points to the audited recovery path. artifact.publish and
work.complete use compatible error details for that condition. Recovery or
operator override may resolve, requeue, or supersede the blocked attempt, but it
does not mutate the historical scope snapshot attached to that attempt.
Run Lifecycle
A run starts in running (after run start). Terminal transitions
that maybe_complete_run produces:
failed— any job in the run reachesstate = 'failed'. The run ends withstop_reason = 'job_failed'.completed— every job is in a terminal state (completed,skipped, orcanceled) and at least one job iscompleted. Partial success counts: a run that finished any work is recorded as completed.canceled— every job is in a terminal state and none iscompleted.recovery cancel-job --cascadeover an entire run is the typical trigger;stop_reason = 'all_jobs_canceled'. Abandoned-run auto-cancel uses the same terminal state with an abandonment stop reason.
Auto-close on a run-terminal transition (RFC 0011) records each
session's close_reason from the same vocabulary: run_completed,
run_failed, run_canceled, or explicit.
Before choosing a clean completion, maybe_complete_run runs the
RFC 0118 run-completion provenance gate: every provenance-required
review gate (require_attested_lane=true or fresh per the reviewer
policy) must show, on its latest non-superseded accepting verdict, a
frozen lane_attestation_at_record='attested' stamp or an explicit
override basis (review_provenance_override, posture='override',
or a run-level accepting escape_surface=review_provenance
decision). A NULL stamp (pre-migration row) is fail-closed. All other
required verdict-capable gates are held to the shipped admission rule
(accepting + present). A failing gate routes the run to
needs_operator with stop_reason='provenance_gate_failed' and a
resolvable escalation enumerating the failing gates; resolving the
escalation re-drives completion. A completed run records
completion_mode — lanes_attested when every provenance-required
gate cleared on a frozen attested stamp, operator_override when any
gate cleared by an override basis. completion_mode is advisory
metadata: downstream consumers that care must read it explicitly.
Verdict rows also carry declared model identity stamps when the owner bundle is
applied: model_identity_declared, model_family_at_record,
model_identity_basis, and model_co_blindness_at_record. These stamps are
derived only from the immutable workflow snapshot's declared lane model
metadata, or recorded as unknown with an explicit operator/recovery basis for
override and recovery paths. Historical NULL rows and deployments without the
owner columns read as unknown; no historical rewrite upgrades old verdicts to
stronger proof.
When a SINGLE job exhausts its autonomous-recovery budget but its
downstream is clear, the recovery decision tree moves ONLY that job to
the non-terminal quarantined state and lets the run
finalize-the-majority on its already-completed deliverables, instead of
flipping the whole run to needs_operator and discarding every
completed sibling's work. A job is quarantine-eligible only when ALL
hold: no unfinished job transitively (multi-hop) depends on it; it is
not a provenance-required reviewer the run-completion gate would refuse;
the per-run cap recovery_policy.max_quarantinable_jobs (default 1) is
not exceeded; and the quarantined job state is permitted by the live
schema (owner bundle 0012 applied). If any guard fails the job falls
through to the unchanged whole-run needs_operator escalation. The
quarantined job is NEVER completed and NEVER has an artifact sealed on
its behalf — it is the one narrow thing surfaced to the operator, who
terminalizes it with recovery accept-quarantined (resolves the blocker
and marks the job canceled-by-operator; idempotent). A finalize-the-
majority run reaches completed (no new run state) carrying a
quarantine manifest on the terminal event and the completion record,
with stop_reason='quarantined_jobs'; a recovery.job_quarantined
event names the offending job + lane + stall_class.
Every terminal transition — including the operator run cancel path —
freezes a write-once runs.completion_record_json INSIDE the terminal
transaction, before session teardown: per-job state and attempt, the
frozen verdict provenance stamps, per-session attestation and
(imminent) close reason, supervisor and lease state, and a
recovery-event summary. The record's sha256 is anchored in the
append-only terminal event payload for tamper evidence.
Per-run serialization invariant (RFC 0104)
Every transaction that mutates a single run's aggregate — its rows in
sessions, runs, jobs, leases, queue_messages, verdicts, blockers,
and interrogations — MUST take a per-run, transaction-scoped advisory lock
(pg_advisory_xact_lock keyed on striatum:run:<repository_id>:<run_id>, via
lockRun) as its first statement, before any FOR UPDATE on a run-scoped
row. This gives the hot per-run write paths an identical, earlier serialization
point so the historical {sessions, runs} deadlock cannot form: the claim path
(work.claim_next / work.await_packet) locks sessions→runs→jobs, while the
verdict-completion path (review.submit / review.verdict → maybe_complete_run
→ close-remaining-sessions), the lifecycle paths (work.complete, run.cancel,
run.retry_job, checkpoint.resolve), and the per-run recovery sweep lock the
same rows in the opposite order. The lock is per (repository_id, run_id) — the
narrowest scope that serializes only the small, lane-bounded write concurrency
within one run; unrelated runs and repositories never serialize against each
other, and the claim queue's FOR UPDATE … SKIP LOCKED is exempt (already
deadlock-safe). The bounded deadlock-retry wrapper is retained only as
defense-in-depth; a surfaced 40P01 is now a should-never-happen signal. A
guard test asserts every per-run mutation handler takes lockRun before any
run-scoped FOR UPDATE.
Sessions
Agents must call register-session before claiming work. Database identity is
an opaque session_id; human display uses <role>-<lane>-<ordinal> slugs.
Sessions match work by run, role, lane, and capabilities. Jobs can require fresh sessions. Native sub-agents spawned inside an agent CLI inherit the parent session unless explicitly registered as first-class sessions.
Session lifecycle and closure
Design rationale: RFC 0011.
Sessions are created active by register-session. The state column
ranges over ('active','expired','stopped','lost','closed'):
active: registered and eligible for work only while its lane backend is still attached and lane-attested. Claim, generic completion, and normal review completion paths refuse to drive work for an active row that has no live attached supervisor backend.expired: an explicit recovery path released the session's lease and marked the session expired, or a claim/completion attempt found no attached supervisor backend for an otherwise-active session.stopped/lost: the session's supervised process exited (RFC 0009).closed: the new terminal state introduced by RFC 0011, set either by the explicitstriatum session closecommand or by run-terminal auto-close.
striatum session close --session-id <id> --reason <text> is idempotent
against an already-terminal session (returns the existing terminal row
plus a note) and refuses with exit 4 when the session still holds an
active lease (the message points the operator at striatum release).
On the happy path it transitions the session to closed, records
closed_at and close_reason, and emits a session.closed event with
payload {session_id, role_id, lane_id, reason, source: "explicit"}.
If the session still has an active attached/detached process supervisor but no
active work lease, the close path first stops the supervisor rows, updates the
daemon/pointer rows to stopped, and emits supervisor.stopped with
source: "explicit_session_close".
When a run transitions to a terminal state (completed, failed,
canceled), the runner automatically closes every still-active session
on the run inside the same transaction. Each auto-close emits a
session.closed event whose source is one of "run_completed",
"run_failed", or "run_canceled". Auto-close skips any session that
holds an active lease — the existing expire_leases/recovery flow
remains the path for those.
The doctor check active_session_on_terminal_run is preserved as the
residual warning for genuinely anomalous states (transition skipped,
manual PostgreSQL editing, partial recovery). After auto-close it should
no longer fire on a clean-finish run.
evidence export and run summary include a per-session block with
each session's state, closed_at, close_reason,
lane_attestation, operator_label, and (when set by HARNESS-003
override) non_fresh_reason. The RUN_SUMMARY.md
## Sessions section lists one line per session in registration order.
Work Queue
claim-next lazily expires active leases, verifies the claiming session is
active and backed by a live lane-attested supervisor, then atomically claims the
oldest eligible pending work message. Generic work.complete and normal review
completion paths (review.submit/review.verdict) apply the same backend gate;
explicit review-provenance decisions remain the documented escape path for
operator-authored review recovery. If an otherwise-active session has no
attached supervisor, these paths mark it expired and refuse the transition. If
its attached supervisor PID is missing, gone, or identity-mismatched, they mark
the supervisor and session lost and refuse the transition. Other unattested
backend failures are refused without claiming or completing work. It returns a
structured work packet and stores the packet JSON plus hash.
work.await_packet is the autonomous MCP agent-loop acquisition surface over
the same authoritative claim transition. When the long-poll wait reaches a
terminal idle result with no eligible work for the session, it returns a
type: "none", status: "no_work" envelope with
idle_behavior: "exit_session". This is a clean lane-exit instruction, not a
request to keep the model process resident and poll again; run drive or a
future scheduler-principal mechanism is responsible for starting a fresh
session when work is queued later. Lanes treat any unrecognized non-empty
idle_behavior value on a no_work envelope as exit_session (fail closed);
only an absent idle_behavior preserves the legacy keep-polling behavior of
older daemons. The D180 notify-only wake bus exposes
wake.wait as a read-shaped hint surface so run drive can block until
committed work, agent-message, or conversation-turn availability is signaled,
then re-read authoritative daemon/PostgreSQL state before launching anything.
Wake hints never claim, lease, complete, verdict, spawn, or otherwise mutate
workflow state.
If a supervised agent exits before its first await and the session is already
stopped, work.await_packet returns a typed status: "no_work" envelope with
reason: "session_stopped", session_state: "stopped", and
next_action: "register_fresh_session" rather than surfacing a transport-level
transition error. The envelope still carries idle_behavior: "exit_session" so
the receiver exits cleanly and leaves queued work for a replacement session.
Every other non-active session state (closed, expired, lost) returns the
same terminal envelope shape with reason: "session_<state>" and the matching
session_state, so a receiver never error-loops against a finished session.
If the first await arrives before supervise.start has attached the session
backend, the await loop waits through the long-poll window and, if the backend
still is not attached, returns reason: "session_backend_not_ready" without
claiming work or expiring the session.
Required transition commands:
ackheartbeatreleaseblockcompleteverdictpublish-artifactsend
Expired review-only leases can be requeued when attempts remain. Expired repo-write leases become stale or blocked and require coordinator or operator inspection before requeue. When the inspection raises an unresolvable question, the operator escalates to the human principal (RFC 0053).
Artifacts
Published artifacts are curated outputs: prompts, findings, ledgers, syntheses, decisions, handoffs, markers, and test reports.
Owner choices can be recorded with decision record. The command writes a
durable Markdown artifact with YAML front matter using
schema_version: striatum.decision.v1, artifact_kind: decision, a stable
decision_id, run_id, outcome, follow_up_required, title, owner, and
creation timestamp. It records the file as a run-level artifact of kind
decision with no job, session, or active lease requirement, and emits a
decision.recorded event. Outcomes are accepted, rejected, and
accepted_with_follow_up; the follow-up outcome requires explicit follow-up
text. For constrained-operator escape hatches, decision record accepts
--escape-surface and --escape-action; both are required together and require
--rationale. Escape decisions add escape_decision: true, escape_surface,
and escape_action to the decision front matter and event payload, turning an
ambient escape into a durable audited run-level decision.
For completed runs whose provenance is later found compromised, an accepting
decision can pass --mark-run-compromised; the daemon records the decision
artifact, emits run.compromised, and transitions the run from completed to
compromised so operators can start a replacement run without silently reviving
finished work. Completed review jobs are not selectively invalidated or retried
in V1; compromised completed review provenance is corrected by marking the
completed run compromised and starting a replacement run.
Durable Markdown artifacts should include the work packet's privacy-safe
author: line in their title block when one is provided. For unattested
sessions this line is author: operator, not a lane/model byline.
publish-artifact validates file existence, repo-relative path, write scope,
artifact kind, and content hash. Transcript artifacts are rejected by default.
Markdown artifacts may include YAML front matter or title-block author:
metadata; when they do, the line must exactly match the work packet's lowercase
author line. The publisher still records artifacts rather than rewriting them.
Artifact placement is explicit when a workflow declares
expected_artifacts[].placement:
blob_exhaust: lane exhaust bodies are stored in the repository's blob bucket when blob storage is configured. The artifact row records blob key, blob sha, content type, and resolved placement.git_publication: source-like or human-reviewable records stay anchored in git throughrepo_pathpluscontent_sha256.git_pointer_manifest: a compact git-retained manifest points to blob artifacts by id/hash/key without embedding lane-exhaust bodies.
Workflows that omit placement remain valid. The compatibility default preserves
the RFC 0072 kind routing: findings, syntheses, ledgers,
harness_improvement_proposal, and progress_note default to blob_exhaust;
other artifact kinds default to git_publication. New generated workflows emit
explicit placement. artifact.publish, artifact reads/listings/exports, and
doctor checks use the same resolved placement instead of treating artifact kind
as the final storage authority.
Generated operator records that are not current product source may be imported
into the daemon-owned generated_records index. The body bytes live in the
repository's blob bucket; the row records source path, source commit, record
class, content hash, blob key/hash, retention class, and import batch. The
records migration import path is daemon-backed and idempotent for safe
historical inventory entries, records migration verify proves reconstructed
blob bytes against the original manifest, and records migration materialize
writes verified copies only under ignored .striatum/scratch. Import and
reconstruction proof do not authorize deleting tracked historical files; deletion
requires a separate operator decision or pilot.
If a lane tries to publish or complete after its Striatum session is already
terminal, the daemon returns session_inactive before lease validation and
points at the daemon-backed same-attempt recovery path: requeue the job, claim
it from a fresh active session, and then retry the publish/complete from that
session. This preserves the usual author-line, write-scope, and artifact
durability checks instead of granting a closed session new write authority.
Model-bylined artifacts require lane evidence: if the daemon supervisor has
reported artifact_observed events for the session, one must match the
published repo-relative path; otherwise clean process_executions rows from
mediated process.run or older wrappers can satisfy process evidence for
wrappers that have not yet reported path-specific observations. The
operator-only --allow-no-process-execution --override-rationale path records both a provenance event and the artifact's
attestation_override_rationale.
Artifact list, detail, summary, export, and dashboard read surfaces include a
derived provenance object alongside the actual byline. Its category is one
of attested_supervised_lane, unattested_no_supervisor_session,
daemon_auto_finalized_from_artifact, operator_published_on_behalf,
operator_self_declared, recovery_authored, operator_authored, or
unknown. The derivation is read-only and uses the artifact row, session
operator labels, publish-time supervisor/process evidence, and provenance or
recovery events; it does not add a new durable state table or upgrade bylines
into proof of source-byte authorship.
complete and review verdict commands verify all required artifacts before
terminal job transition.
submit-review composes the common review path: it publishes the review
artifact, records the verdict, applies review-gate behavior, and returns the
artifact, verdict, blocker, run, and downstream state. It is for the normal
case where the reviewer is publishing the finding artifact during this call. If
a re-claimed review job already has its required finding artifact published for
the current attempt, use verdict / review.verdict instead; it records the
verdict against the existing artifact and avoids re-publishing an immutable
logical name.
For an unattested session recording an accepting verdict on a fresh review job,
or on a review job that declares require_attested_lane: true, submit-review
and verdict require --review-provenance-decision-id <id> unless the session
is lane-attested. The referenced artifact must be a run-level accepting
decision whose decision.recorded payload declares
escape_surface: review_provenance. When accepted, the daemon records
review_provenance_override, the decision id, and the decision artifact id on
the verdict.recorded event. Direct publish-artifact remains strict for
require_attested_lane; use submit-review for the audited combined override
path.
override-verdict is an explicit operator recovery command for a completed or
waiting_human review job whose latest verdict is non-accepting. It requires a
fresh active session on the same run, appends a newer accept or
accept_with_findings verdict without editing prior verdict rows, resolves
revision-routing human checkpoints when present, and re-evaluates downstream
gates. The appended override verdict records model identity as unknown with
an operator basis; checkpoint and recovery-created clearing verdicts follow the
same fail-toward-unknown rule rather than fabricating a model from later state.
checkpoint.resolve continue and checkpoint.resolve override refuse before
mutating blocker or job state when the parked waiting_human job declares a
required artifact whose body is not inspectable from its declared placement.
The refusal returns the affected artifact ids, paths, placements, readback
status, and recovery guidance. cancel remains available as the operator's
explicit stop path; advancing or overriding a checkpoint may not route around
missing official evidence.
evidence export writes a redacted Markdown snapshot of run, job, blocker,
verdict, artifact, status, doctor, and downstream-blocking state. Export paths
must stay inside the repository and outside .striatum/; the daemon DB
remains live state outside the repository, and .striatum/ scratch or
migration tombstones are not durable provenance. Free-text fields that may
contain agent or user prose, including blocker descriptions and verdict
rationales, are redacted in the export.
Workflow job titles are omitted by default; job and artifact authorship is
reported through stable identity metadata: role id, lane id, declared model
display name, and workflow job id.
Verdict model identity stamps are exported as categorical scalar fields and
remain redaction-safe; free-text verdict rationales remain redacted.
Evidence redaction is default-deny. The export schema is defined by an
explicit per-field policy registry that classifies every emitted field as
safe, redacted, or dropped. Any field added to evidence_snapshot(),
status(), or doctor() that is not registered as safe is replaced with
the redaction placeholder. New fields cannot leak agent or user prose into a
committed export without an explicit, reviewable change to the registry.
safe is scalar-only: if an emitted safe field unexpectedly contains an
object or list, the exporter replaces it with the same placeholder instead
of recursively trusting nested content.
Work packets expose an exact lowercase author: line for agents to place in
durable Markdown artifacts. This byline is distinct from evidence-export
identity metadata: exports keep stable role id, lane id, declared model display
name, and workflow job id; artifact files use the compact
author: <role-name>-<model-name>-<ordinal> convention so workflow job titles
or other project-specific prose do not leak into the artifact byline. The
artifact publisher records and validates artifact references; it does not
rewrite artifact files to insert headers.
Artifact Front Matter Schemas
Durable Markdown artifacts may include an optional YAML-style ----delimited
front-matter block at the top of the file. When the artifact kind has a
registered schema and a front-matter block is present, publish-artifact
validates the parsed metadata against the schema. Files without a front-matter
block remain accepted as before, except collaboration_ledger where the
front-matter block is required because the verdict gate reads structured
metadata. The publisher never rewrites artifact files.
Front-matter values are parsed as YAML. The older JSON-compatible scalar and
list style (key: "value", flag: true, items: ["a", "b"]) remains valid,
and schemas may now opt into nested mappings or list-of-mapping rows where the
contract declares them.
Standard optional metadata (RFC 0100). Any artifact kind tolerates a common
set of byline/workflow metadata keys without rejection, so a lane can keep the
natural front matter its template produces instead of reverse-engineering each
kind's schema: author, workflow, phase, lane, role, model, date,
created_at, updated_at, visibility, title, status, tags, summary,
related, run_id, session_id, ordinal, cycle. These are accepted
free-form (not value-checked) unless a specific kind gives one of them a
required, checked meaning (e.g. decision.title, decision.run_id). A field
that is neither in the kind's schema nor in this standard set is still rejected,
but the rejection now names the kind's required keys, optional keys, and the
standard-metadata set — no source reading required.
V1 schemas:
-
striatum.decision.v1(kinddecision): requiredschema_version,artifact_kind: decision,decision_id,run_id,owner: human,outcome(one ofaccepted,rejected,accepted_with_follow_up),follow_up_required(boolean),title,created_at; optionalescape_decision(boolean),escape_surface, andescape_actionfor constrained-operator escape decisions. -
striatum.finding.v1(kindfinding): requiredschema_version,artifact_kind: finding, andverdict_intent(one ofaccept,accept_with_findings,needs_revision,reject); optionalseverity(one ofinfo,low,medium,high,critical) andtags(list of strings). -
striatum.findings_ledger.v1(kindfindings_ledger): requiredschema_version,artifact_kind: findings_ledger, andsummary_count(non-negative integer); optionalentries_path. Ledger entries themselves are body content, not structured front matter. -
striatum.synthesis.v1(kindsynthesis): requiredschema_versionandartifact_kind: synthesis; optionalinputs(list of logical-name strings). -
striatum.support_ledger.v1(kindsupport_ledger, RFC 0003): requiredschema_version,artifact_kind: support_ledger, andaudited_artifact(string repo-relative path or logical name); optionalclaim_count(non-negative integer). Ledger rows themselves are body content. -
striatum.action_item_ledger.v1(kindaction_item_ledger, RFC 0004): requiredschema_version,artifact_kind: action_item_ledger,source_review_artifact(string), andrevision_round(non-negative integer); optionaltotal_items(non-negative integer). Action-item rows themselves are body content. -
striatum.harness_improvement_proposal.v1(kindharness_improvement_proposal, RFC 0005): requiredschema_version,artifact_kind: harness_improvement_proposal,target(one ofprompt,workflow,spec,defaults,documentation), andexpected_benefit(string); optionalriskandrollback(strings). -
striatum.operator_brief.v1(kindoperator_brief): requiredschema_version,artifact_kind: operator_brief,brief_id,supersedes(string or null),scope_links(list of repo-relative strings),context_budget_lines(non-negative integer),retrieval_priority(one oflow,medium,high), andstatus(one ofcurrent,superseded); optionalauthor. -
striatum.work_plan.v1(kindwork_plan): requiredschema_version,artifact_kind: work_plan,plan_id,scope_kind(one ofrfc,phase,initiative,bugfix),scope_ref,state(one ofopen,in_progress,closed),opened_at,closed_at(string or null),closure_summary(string or null),supersedes(string or null), andretrieval_priority; optionalauthor. -
striatum.progress_note.v1(kindprogress_note): requiredschema_version,artifact_kind: progress_note,note_date,session_slug,related_plan(string or null),related_brief(string or null), andretrieval_priority; optionalauthor. -
striatum.operator_report.v1(kindoperator_report): requiredschema_versionandartifact_kind: operator_report; optionalauthor,retrieval_priority, andsupersedes(string or null). -
striatum.escalation.v1(kindescalation, RFC 0053): requiredschema_version,artifact_kind: escalation,escalation_id,run_id,severity(one ofblocked,human_checkpoint),blocker_kind(one ofambiguous_goal,missing_authority,contradicting_decisions,no_available_reviewer_lane,committee_stalemate,override_required,ai_self_declared),description,reasoning,requested_action, andcreated_at; optionaljob_id,session_id, andrelated_artifacts(list of strings). These artifacts are AI-authored escalation requests for the human principal; they do not create a dedicated live-state table. When published through daemonartifact.publish,escalation_idis treated as the target blocker id for an existing escalation-class blocker in the same repository/run. Successful linkage stores compact metadata underblockers.payload_json.escalation_artifactand the escalation inbox projections surface it; publishing an escalation artifact does not create a new live blocker by itself. Livework.blockmutations validate the blocker request shape and persiststriatum.blocker_payload.v1metadata underblockers.payload_json; escalation-class blockers mirror the same payload intoescalation_inbox.payload_json. -
striatum.commit_request.v1(kindcommit_request, RFC 0067 / D127): requiredschema_version,artifact_kind: commit_request,request_id,base_head,branch,git_snapshot_hash,included_paths(non-empty list of strings),commit_message,rationale, andconfirmation_status(one ofpending,operator_confirmed,human_confirmed,refused); optionalrun_id,reviewed_artifacts,confirmed_by, andconfirmed_at. Daemongit.commit_applyaccepts only explicitly confirmed local commit requests and never pushes. -
striatum.pr_request.v1(kindpr_request, RFC 0067 / D127): requiredschema_version,artifact_kind: pr_request,request_id,target_branch,summary,body_draft, andconfirmation_status(one ofpending,human_confirmed,refused); optionalrun_id,related_commit_request,local_commit_sha,provider_target,confirmed_by, andconfirmed_at. Core Striatum records the request artifact only; hosted provider actions remain out of core. -
striatum.auto_finalize_gate_evidence.v1(kindauto_finalize_gate_evidence, D125): requiredschema_version,artifact_kind: auto_finalize_gate_evidence,decision_id: D125,gate_status(one ofpending,satisfied),live_success_count,lane_shape_count,lane_shapes,contested_audit_chain_events,evidence_artifacts, andcreated_at. Asatisfiedartifact must record at least three live successes, at least two lane shapes, and zero contested audit-chain events. This artifact records the default-on evidence gate; it did not itself make live auto-finalize the global default. D133 is the separate decision that flips default-live allowance after the gate is satisfied. -
striatum.join_manifest.v1(kindjoin_manifest, RFC 0133 Slice 1 / RFC 0135 P0, D213 / D216): requiredschema_version,artifact_kind: join_manifest,barrier_id,entity_kind(one ofjob,review_seat,review_obligation,run),run_id,base_oid,tree_sha,commit_sha,created_at, andin_edges(non-empty list of objects). It is the provenance artifact emitted as the sole output of a sealed-barrier fire — what was joined, never an understanding of it (semantic netting is an explicit non-goal). Eachin_edgesrow records its SEALED contribution: requiredentity_id(the stable entity id),seal(integer — the LIVE seal this contribution joined at, so a superseded-seal contribution is provably absent from the join), andstatus(one ofstaged_live,quarantined,dead_seat). Astaged_liverow additionally requirescommit_shaandstaging_ref; a terminal-gap row (quarantinedordead_seat) requires adamage_codeexplaining the gap. This artifact registers as anartifactcontractscontract with the publisher's exit-code-6 schema guard and creates no live-state table (no DDL). The sealed barrier predicate itself —staged.seal = live.seal, never a per-entityCOUNT(*)of staged refs — is minted once inpkg/db(BarrierReadySQL) and protected by a static build guard; P0 ships the manifest contract and the predicate shape with no live caller yet (fan-in is the first live caller in P1). -
striatum.claim_ledger.v1(kindclaim_ledger, RFC 0134 lattice slice / D227): requiredschema_version,artifact_kind: claim_ledger,run_id,ledger_seal(non-negative integer — the monotonic, append-only seal), andclaims(non-empty list of objects); optionalcreated_at. Eachclaimsrow carries requiredid(ledger-unique),status(one ofDESIGNED,ASSERTED,VERIFIED— the claim latticeVERIFIED > ASSERTED > DESIGNED), andtext, plus optionalbound_input_digest,receipt_ref, andevidence_digest. The provenance lint (at the publisher, exit code 6) refuses a claim whose status exceeds its evidence: aVERIFIEDclaim requires a non-emptyreceipt_ref, a non-emptybound_input_digest, and anevidence_digestequal to it (the receipt is bound to these inputs); aVERIFIEDclaim that fails to bind reads back asASSERTED(auto-decay), so an unwitnessed claim asserts at mostASSERTED. The daemon writer additionally enforces the cross-seal lattice rules against the prior ledger: theledger_sealmust strictly exceed the most recent prior ledger's seal (append-only), and a claim (matched byid) is demotable but never self-promotable — its status may be lowered across seals but raising it to a higher rung is refused (promotion is the sealed mint, off the gate path). This is the validation-only half of RFC 0134 (the claim-status lattice): the daemon validates and reads prior ledgers, it never executes a check, and there is noverifyjob type that runschecks[]in this slice (the executable verifier is a later, sandboxed off-gate-path lane). -
striatum.receipt.v1(kindreceipt, RFC 0134 / D227): the sealed evidence aVERIFIEDclaim binds to. Requiredschema_version,artifact_kind: receipt,check_id,argv(non-empty list of tokens),binary_sha256,exit_code(non-negative integer),stdout_sha256,cwd_tree_sha, andseal_digest; optionalcreated_at. It records the tamper-evident transcript shape (argv + resolved binary hash + exit code + stdout digest + cwd tree-sha) so the provenance lint can bind a claim'sevidence_digestto the receipt'sseal_digest. In this validation-only slice the daemon does not execute checks to produce a receipt — a receipt is authored/sealed off the gate path by a sandboxed verifier lane (a later slice); the claim ledger merely names it. -
striatum.advisory_minority_report.v1(kindadvisory_minority_report, RFC 0132 Layer C / P4b, D214): requiredschema_version,artifact_kind: advisory_minority_report,run_id,gate_workflow_job_id,guard_fired(one ofadvisory_corroborated_abstention,unanimous_advisory_reject,advisory_only_panel_ungrounded,none),advisory_outcome(one ofmust_escalate,advisory_noted),created_at, andseats(non-empty list of objects). Eachseatsrow records one advisory seat'sworkflow_job_id,verdict(one ofaccept,accept_with_findings,needs_revision,reject,abstain), and an optionalrationale_excerpt. Theadvisory_outcomemust agree withguard_fired: a fired guard (anything butnone) requiresmust_escalate;nonerequiresadvisory_noted— so a report cannot claim a clean silent-accept while naming a guard that blocked finalize. It is the mandatory minority report every panel finalize authors (advisory: loud reject, silent accept, never silent), pinned as a requiredexpected_artifactso the publisher refuses with exit code 6 if omitted. It registers as anartifactcontractscontract and creates no live-state table (no DDL). -
striatum.collaboration_ledger.v1/striatum.collaboration_ledger.v1.1(kindcollaboration_ledger, RFC 0093 / RFC 0098): requiredschema_version,artifact_kind: collaboration_ledger,shape(one offalsification_gate,cross_examination,fog_of_war_review,synaptic_prune,adjudicated_constraint_extraction),topic,participants(non-empty list),entries(list of objects with requiredkind,by,refs, andtext),verdict(one ofaccept,accept_with_findings,needs_revision,reject), andrationale. Entry kinds areclaim,challenge,rebuttal,constraint, ornomination;bymust name a participant; every ref must be adialogue:<sequence>turn reference. Clearing verdicts must include at least one referencedclaim,challenge, andrebuttal.review.submitrejects acollaboration_ledgerartifact when the submitted verdict differs from the ledger front-matter verdict.v1.1is additive. Optional fields arecycle(non-negative integer),findings[],constraints[],branches{}, and the RFC 0094 §5 adjudicator-reliability extrasadjudicators[]andadjudication_mode(below).shape: adjudicated_constraint_extractionrequiresschema_version: striatum.collaboration_ledger.v1.1; if that shape publishesverdict: needs_revision,constraints[]must include at least one productive row (binding: trueorkind: unresolved_question). The refined RFC 0098 statesblocked_pending_answeranddefer_with_successorare validbranches{}dispositions, not validverdictvalues.RFC 0094 §5 adjudicator-reliability extras raise the anti-theater bar from structural presence to semantic correspondence (additive
v1.1; V1 ledgers stay valid):- Check-B is the semantic challenge↔rebuttal rubric. Each
challengeentry may carry an optionalcorrespondencejudgment, one oflanded_and_rebutted(the rebuttal materially addressed the specific gap),landed_unrebutted(a confident non-rebuttal that did not), ornot_material.correspondenceis only valid on achallengeentry. Once any entry records acorrespondence(Check-B engaged), a clearing verdict (accept/accept_with_findings) requires at least onelanded_and_rebuttedand nolanded_unrebutted; otherwise the publisher refuses with exit code 6. Ledgers that record nocorrespondencekeep the pre-existing structural-presence clearing rule. - For
fog_of_war_review, aconstraint/scoring entry may carry an optionalcoverage, one ofreconstructed,hallucinated, ormissed(the judge's ground-truth score of a reconstructed constraint). adjudicators[](optional non-empty list of session ids) records the adjudicator(s) behind the verdict;adjudication_mode(optional, one ofsingleorsecond_on_disagreement) selects the gate. Undersecond_on_disagreementa clearing verdict must be backed by at least two distinct adjudicators (RFC 0064 diversity); a contested clear is conservatively recorded asneeds_revision(which does not require the second adjudicator).single(or an absent mode) is unchanged.
findings[]rows requireid,severity(low,medium,high,critical),posture(non-empty string),status(open,answered,accepted,rejected,converted_to_constraint,deferred_with_owner), andchallenge. Optional keys areclosest_acceptable_answer,affected_invariants,requested_constraint_shape,requires_convener_rebuttal, andsource_refs.constraints[]rows requireid,posture(non-empty string),severity,kind(invariant,gate,schema,policy,non_goal,accepted_risk,unresolved_question),binding, andtext. Optional keys aresource_finding,source_refs,verification, andfinal_review_required. Whenbinding: true,source_findingmust resolve to a same-ledgerfindings[]row withseverity: highorseverity: critical, andverificationmust contain a non-emptygateorexpected_stage.branches{}is a map from posture string to one ofcleared,cleared_with_constraints,blocked,blocked_pending_answer, ordefer_with_successor.Example productive
needs_revisionledger:--- schema_version: "striatum.collaboration_ledger.v1.1" artifact_kind: "collaboration_ledger" shape: "adjudicated_constraint_extraction" topic: "RFC 0098 build" participants: ["sess_builder", "sess_reviewer"] entries: - kind: claim by: sess_builder refs: ["dialogue:1"] text: "The contract is implemented." - kind: challenge by: sess_reviewer refs: ["dialogue:2"] text: "The gate lacks a regression test." cycle: 1 verdict: "needs_revision" rationale: "The revision needs a concrete gate." findings: - id: F1 severity: high posture: implementation status: converted_to_constraint challenge: "The gate lacks a regression test." constraints: - id: C1 source_finding: F1 posture: implementation severity: high kind: gate binding: true text: "Add a regression test for naked needs_revision ledgers." verification: gate: "go -C go test ./pkg/artifactcontracts/..." - id: Q1 posture: product severity: medium kind: unresolved_question binding: false text: "Confirm whether this shape should reopen a revision cycle." branches: implementation: blocked_pending_answer product: defer_with_successor ---Example clearing ledger:
--- schema_version: "striatum.collaboration_ledger.v1.1" artifact_kind: "collaboration_ledger" shape: "adjudicated_constraint_extraction" topic: "RFC 0098 build" participants: ["sess_builder", "sess_reviewer"] entries: - kind: claim by: sess_builder refs: ["dialogue:1"] text: "The contract is implemented." - kind: challenge by: sess_reviewer refs: ["dialogue:2"] text: "The gate lacks a regression test." - kind: rebuttal by: sess_builder refs: ["dialogue:3"] text: "The regression test now covers publish and review paths." cycle: 1 verdict: "accept_with_findings" rationale: "The challenge was answered on the record." branches: implementation: cleared_with_constraints ---Example Check-B + second-adjudicator clearing ledger (RFC 0094 §5): the challenge carries a
correspondencejudgment and the high-stakes gate runs undersecond_on_disagreementwith two distinct adjudicators:--- schema_version: "striatum.collaboration_ledger.v1.1" artifact_kind: "collaboration_ledger" shape: "falsification_gate" topic: "ship gate" participants: ["sess_holder", "sess_falsifier", "sess_adj_a", "sess_adj_b"] entries: - kind: claim by: sess_holder refs: ["dialogue:1"] text: "The proposal is ready to ship." - kind: challenge by: sess_falsifier refs: ["dialogue:2"] text: "The migration path is unproven." correspondence: landed_and_rebutted - kind: rebuttal by: sess_holder refs: ["dialogue:3"] text: "The migration is proven by the linked dry-run fixture." adjudication_mode: second_on_disagreement adjudicators: ["sess_adj_a", "sess_adj_b"] verdict: "accept" rationale: "The only landed challenge was materially rebutted; both adjudicators agreed." --- - Check-B is the semantic challenge↔rebuttal rubric. Each
Other artifact kinds (prompt, marker, handoff, patch_summary,
test_report, other) remain unschemaed in V1 and pass through without a
front-matter check.
Artifact kinds are validated by runtime contract code rather than by SQL
CHECK. Migration version 5 dropped the
CHECK (artifact_kind IN (...)) clause from the artifacts table.
The Go runtime uses go/pkg/artifactcontracts as the shared allowed-kind
and front-matter schema package; the legacy Python contract module is
retired.
publish-artifact and workflow validation reject kinds outside the canonical
set.
Corpus Export And Augmentation Boundary
striatum corpus export --since <ref> --out <dir> (RFC 0044 V1) emits a
redacted JSONL bundle of Striatum's durable provenance — RFCs, decision-log
rows, operator reports, run summaries, audit-chain entries, changelog
entries, ubiquitous-language terms, harness-friction patterns, and recent
commits — plus a verifying manifest.json with per-file row counts, SHA-256
hashes, explicit state_authority metadata for the daemon/PostgreSQL
authority, and a derived bundle_sha256. The bundle is read-only durable
provenance, not live state, and re-running the export over unchanged inputs
produces byte-identical JSONL with stable hashes (generated_at is the only
allowed timestamp variation and is excluded from the bundle digest).
Corpus exports are produced on operator demand. Striatum does not stream
runtime events to any external consumer and does not call any external
service during a run. Bundles live wherever the operator points --out;
nothing under .striatum/ is written by the verb. Corpus source-path checks
deny transcript/output/private path shapes case-insensitively.
The export is an augmentation boundary, not a runtime dependency. An
external memory or retrieval system (Engram is the first reference consumer
under RFC 0044) may ingest a bundle and serve retrieval over its rows, but
the Striatum runner does not import any consumer client library, register
any memory.* capability, or call any retrieval surface during state
transitions. The non-negotiable invariants are:
- No
import engramorfrom engramin Striatum source. - No
memory.*capability in the Striatum daemon method registry. - No state transition (
ack,publish-artifact,complete,verdict, recovery,run prepare,run start,corpus export) that fails when an external memory consumer is missing, unreachable, or misconfigured.
These invariants are pinned by Go guardrails around the corpus export path and the daemon method registry. The contract version, multi-corpus identity, redaction-tier metadata, incremental-export watermark, and optional context-injection policy that power V2 are scoped by RFC 0057.
Warm-Tier Memory
RFC 0119 / D179 splits memory into three authority axes:
- Canonical authority: Striatum state is still PostgreSQL, git, and daemon-owned blobs. The warm tier is never canonical and cannot decide a workflow transition.
- Index scope: local memory consumers may index redacted delivered provenance, including durable-provenance rows, but Striatum exports and reads only its own bounded projections.
- Eviction scope: git eviction is an explicit per-kind taxonomy, finalized in S12 (RFC 0119 / S11 deferred enforcement to this step). Durable provenance is canonical in git and is never evictable; only run exhaust and unsynthesized intermediates are eviction-eligible. The classification is fail-safe — an unrecognized kind is treated as durable.
Eviction taxonomy
The eviction axis is independent of the placement axis above: where an artifact
body is stored (blob_exhaust vs git_publication) does not change whether the
artifact is durable provenance. The taxonomy is enumerated in code at
go/pkg/artifactcontracts/eviction.go (IsDurableProvenanceKind,
IsEvictableKind) and pinned by eviction_test.go.
- Durable provenance (never evictable, stays committed in git): RFCs and the
decision log (git documents, durable by construction), and the artifact kinds
decision,finding,synthesis,operator_brief,work_plan,escalation, andhandoff. Accepted decisions and findings are canonical. - Eviction-eligible (run exhaust + unsynthesized intermediates):
progress_note,operator_report,test_report,auto_finalize_gate_evidence,findings_ledger,support_ledger,action_item_ledger,collaboration_ledger, andharness_improvement_proposal. These are per-run reports, working ledgers, gate evidence, and unsynthesized proposals — the durable provenance they cite (accepted decisions/findings) is recorded separately and stays in git.
A kind absent from both sets defaults to non-evictable, so a future or unrecognized kind is never silently removed from git. This taxonomy moves no durable-provenance kind out of git; it only names the run-exhaust kinds that were already eviction scope under the three-axis model (hippo D003).
Striatum's native hot-tier read surface is recall.search / RecallMemory.
It is a read-only Postgres full-text search over daemon-owned artifact
metadata (logical_name, artifact_kind, author_line, and placement/job
context). It does not read artifact bodies, does not use pgvector or hosted
retrieval, and does not import Hippo, Fornix, Engram, or any other external
memory consumer. The RPC is registered as capability read with
single_repo scope and deliberately does not introduce any memory.* method
or capability.
Scaffold-time recall digest injection is optional and fail-soft. It runs after
worktree.create has committed the authoritative job_worktrees row and
event, so a recall read failure cannot orphan or block the worktree transition.
The daemon controls it with these default-off settings:
STRIATUM_RECALL_DIGEST/--recall-digestSTRIATUM_RECALL_DIGEST_LIMIT/--recall-digest-limitSTRIATUM_RECALL_DIGEST_TIMEOUT_MS/--recall-digest-timeout-ms
The worktree-create response reports a memory_digest status object whose
status is skipped (default-off or no recall), empty, rendered, or
failed; the digest is advisory markdown with local artifact provenance.
corpus.export remains default artifact-only output. Passing
include_lane_trajectory=true adds a separate lane_trajectory class built
from the daemon's curated trajectory projection. This class is redacted,
path-guarded, UTF-8 normalized, token-scrubbed, sorted deterministically, and
manifest-hashed. It intentionally supersedes the transcript/output path-denial
guard only for this curated projection: raw PTY logs, provider output,
terminal transcripts, .striatum/ scratch, hosted-service state, and external
memory state still do not enter the corpus.
The warm-tier boundary is regression-pinned by Go guardrails: no external
memory-consumer imports in daemon source, no memory.* registry or contract
entry, and no state transition dependency on recall or an external memory
consumer.
striatum archive create --run-id <id> --out <dir> is the Phase 11 run
archive foundation. It is a daemon/Postgres-backed read command that writes
a local archive directory for one run: run row, workflow snapshot,
run-scoped rows including command requests, process executions, job
worktrees, process supervisors, process supervisor pointers, artifact
metadata, event metadata, and a self-verifying manifest.json. New archives
advertise archive_contract_version=2, verification_depth=deep_chain,
hybrid archive defaults (snapshot, event_log, and
verify_replay_by_default all true), and artifact_content_policy: metadata_only. The archive does not copy artifact contents, transcripts,
.striatum/ scratch, or any external-service state.
The current Go CLI exposes archive creation only. Local archive verify and
archive inspect verifier commands are not part of the active command surface;
adding them requires a source change plus CLI/reference-doc updates.
Branches And Commits
Workflow startup is gated by the workflow's branch.mode setting.
branch.mode is a closed enum: "auto" (the default when omitted) or
"confirm".
Auto mode (default). When the workflow declares branch.mode: "auto"
or omits the mode field, run prepare atomically:
- Validates and snapshots workflow JSON.
- Ensures the local branch ref
<suggested_name>exists at the run's recorded base commit usinggit branch, without checking it out. - Records the branch and transitions the run to state
ready.
The response includes branch_mode: "auto", the resolved branch,
branch_created (true only when a new branch was created), and the
current_git_branch for cross-check. If git ref creation fails (conflicting
branch name, invalid base, invalid branch name), the run remains in
needs_branch_confirmation and the operator can resolve the issue and
run striatum branch confirm manually. Auto mode requires
branch.suggested_name to be set.
Confirm mode (opt-in). When the workflow declares
branch.mode: "confirm":
run preparevalidates and snapshots workflow JSON and leaves the run inneeds_branch_confirmation.branch confirmrecords explicit operator confirmation and optionally creates or selects a branch. Per RFC 0053 the operator is the AI agent by default; the human principal is not required for branch confirmation.run startmakes eligible root jobs claimable.
Use confirm mode for workflows that require operator review of the target repository state before any branch is touched (e.g., RFC-style spec reviews where the branch is part of the deliberation).
No job is claimable before branch confirmation. Branch confirmation itself does not commit, push, merge, or rebase.
branch confirm --json includes the requested branch and detected current git
branch, warns when they differ, and reports whether the confirmation was
records_only. The default confirm path ensures the branch ref exists at the
run's recorded base using git branch without checking it out; records_only
is false only when that call creates a missing ref. Three flags adjust the
confirmation gate:
--create: explicitly run the same ref-only create-if-absent operation. If git refuses, the runner exits withWorkflowError(code 8) and does NOT record the confirmation. The response fieldcreatedistrueonly when a new branch ref was created.--use-current: ignore--branchas a target and record the current git branch instead. If--branchis also given and disagrees with the current branch, exit with code 8.--strict: require that the current git branch matches--branchexactly before recording. If they differ, exit with code 8 and do not record. This is the safe default for CI and other automation.
The response also includes a mode field
("records_only" | "create" | "use_current" | "strict"). The default
mode name preserves backwards compatibility for existing callers, but the
records_only boolean is authoritative.
git snapshot --json is a daemon read-only local Git projection for the
registered target repository. It reports branch, HEAD metadata, dirty counts,
changed paths, and bounded ancestry without fetching, pushing, committing,
reading remote URLs, importing hosted-provider SDKs, or including diff hunks
or commit bodies.
git commit-apply <commit-request-path> --confirm --confirm-request-id <id> --json is the only core local commit creation surface. It is daemon-routed,
requires apply capability, consumes a repository-relative
striatum.commit_request.v1 artifact, and refuses unless the artifact's
confirmation_status is operator_confirmed or human_confirmed and the
CLI confirmation id matches the artifact request_id. Before creating a
commit, the daemon verifies that current HEAD equals the artifact base_head,
the current branch equals the artifact branch, every dirty path is within
the artifact included_paths, and all included paths stay inside the
repository and outside .striatum/. The commit is local only, uses the
artifact commit_message, stages only included_paths, disables repository
Git hooks for the commit invocation, and returns the local commit SHA. It
does not push, fetch, open or update PRs, call hosted providers, import
provider SDKs, or load provider credentials.
CLI
Required commands, grouped by concern:
# Core lifecycle
striatum repo add
striatum workflow validate
striatum workflow generate
striatum workflow templates
striatum run prepare
striatum branch confirm
striatum run start
striatum run drive
striatum run summary
striatum run graph
# Agent / session work loop
striatum register-session
striatum claim-next
striatum ack
striatum heartbeat
striatum release
striatum send
striatum block
striatum publish-artifact
striatum submit-review
striatum complete
striatum verdict
striatum override-verdict
striatum decision record
# Worktree (opt-in per lane)
striatum worktree create
striatum worktree release
striatum worktree gc
striatum worktree list
# Supervisor (RFC 0009)
striatum supervise start
striatum supervise send
striatum supervise stop
striatum supervise status
striatum supervise list
# Dashboard
striatum dashboard
# Inspection and recovery
striatum status
striatum why
striatum doctor
striatum evidence export
striatum recovery stale-leases
striatum recovery requeue-stale
striatum recovery resume
# Corpus export (RFC 0044 V1; RFC 0052 contract)
striatum corpus export --since <ref> --out <dir>
striatum archive create --run-id <id> --out <dir>
# Adapter
striatum adapter run
Human read commands can pretty-print. --json returns stable machine-readable
JSON. Mutation commands support JSON output for agent use.
striatum run drive --run-id <id> [--interval 15s] [--provider-auth-gate auto|required|off] [--once] [--json] is a
CLI-local operator loop over existing daemon RPC methods. It re-reads run
detail and sessions, registers/supervises fresh role-lane sessions as queued
jobs unblock, adopts already-active matching sessions, and stops terminal or
superseded launched lanes before fresh-reviewer registration. It is not a new
daemon RPC method and does not use recovery, retry, override, or force-non-fresh
verbs. The provider-auth gate mode is forwarded to supervise.start; a blocking
lane-provider-auth refusal closes the freshly registered session, reports a
sanitized action, and stops the driver invocation.
Introspection
status --json keeps aggregate run and job counts and also reports open
blockers, human checkpoints, latest non-accepting review verdicts, claimable
jobs grouped by role and lane, blocked downstream jobs, and deterministic
next_actions.
why <id> --json resolves run, job, queue message, blocker, artifact,
verdict, session, and process ids. Blocker introspection includes owning
context, related verdict when present, blocked downstream jobs,
human-checkpoint context when relevant, and next actions.
Doctor And Verbose Records
doctor [--verbose] returns stable string problems, warnings, and
notices lists by default. problems are ok-reddening integrity or liveness
breaches. warnings are non-red findings that still name actionable operator
work. notices are non-red advisory facts such as acknowledged historical
provenance or local launch-environment hygiene that do not name daemon recovery
work. With --verbose the payload also carries problem_records,
warning_records, and notice_records lists of structured records with stable
check names (e.g. active_job_without_active_lease,
stale_queue_message_claim, worktree_path_missing_on_disk,
worktree_head_unreachable, job_completed_without_anchor,
supervisor_pid_missing, supervisor_stdin_pipe_missing), the affected id,
and a small context map. The Go CLI passes --verbose through to daemon
doctor; without it, the string lists remain the stable compatibility surface.
The problems string list is preserved verbatim so callers that already grep
problems keep working.
Dashboard
striatum dashboard --run-id <id> renders a compact, dependency-free terminal
view over the same daemon-owned PostgreSQL state that status and why
expose. It refreshes every 2 seconds by default and shows run state and branch,
job counts by state, verdict counts, open blockers (including human
checkpoints), claimable work grouped by role/lane, deterministic next actions,
and the most recent events. --refresh <seconds> changes cadence; --once
renders a single frame to stdout and exits, which makes the dashboard useful in
scripts and CI assertions that should not redraw a TUI.
The --once JSON frame carries the RFC 0157 (D251) canonical
state_projection block — {run_state, jobs:[{id,state}]} — and a top-level
state scalar that mirrors state_projection.run_state, so a script reads the
dashboard's run state at the top level (parity with the rendered view) without
descending into jobs_by_state. The projection is additive: jobs_by_state,
blockers, open_blockers, recent_events, and the rest are unchanged. When no
run is in scope (no --run-id and no run exists for the repository), both
state and state_projection.run_state are null and state_projection.jobs
is empty. See Operator read-surface state projection.
When the terminal is at least 100 columns wide and 30 lines tall and the
run's workflow has at least one edge, the dashboard appends a graph
panel: a layered ASCII view of the workflow's job DAG annotated with each
job's current state (highest-attempt jobs.state per workflow_job_id).
State letters are Q/R/C/B/H/F/P/X/S for
queued/running/completed/blocked/waiting_human/failed/pending/canceled/stale_lease.
needs_revision cycles render after the layered grid as dashed ~~>
arrows. Auto-detection can be overridden with --graph / --no-graph;
--graph-only hides the rest of the frame; --graph-style {auto,layered,list,fancy} forces a layout — fancy uses Unicode
box-drawing characters (┌, ┐, └, ┘, ─, │, with ╌╌▶
for cycle back-edges) and falls back to layered when the per-slot
width drops below 14; --graph-orient {tb,lr} picks orientation —
tb (default) is top-to-bottom; lr arranges layers as columns
with ─→ separators (or -> in non-fancy mode) and falls back to
tb when per-column width drops below 14; --graph-no-cycles
suppresses back-edges. ANSI 16 colors mirror the existing Mermaid
state palette and are emitted only on TTY output and only when
NO_COLOR is unset (de-facto standard); --once is non-TTY by
construction. The same renderer powers striatum run graph --run-id <id> --format ascii [--graph-style ...] [--graph-orient ...] for
one-shot snapshots that share the same shape as the dashboard
panel.
Run Summary
run summary writes a compact durable Markdown note with run id, branch
context (recorded plus current git branch with an explicit (MISMATCH)
annotation when they differ), run timing (created_at, started_at,
completed_at, and a wall-clock duration), job counts, verdicts grouped by
review job with attempt counts, artifacts annotated with structured author
bylines, blockers, and verification state. The renderer is deterministic so
two runs with the same daemon state produce the same Markdown.
run.summary carries the verdicts' frozen provenance stamps, the run's
completion_mode, an overrides[] block (every override-cleared verdict
with its authorizing decision), and a sessions[] block. On a terminal run
the sessions block and completion_record projection come from the frozen
run_completion_record — the last-live state captured before teardown —
never from a live probe. evidence export renders deterministic
## Sessions, ## Provenance Gate, and ## Operator Overrides sections
from the same frozen state. Verdict projections in run.summary,
completion_record, the provenance gate ledger, and evidence export include
the declared model identity fields and the co-blindness qualifier, using
unknown for historical or unstamped rows.
run.summary --json also carries the RFC 0157 (D251) canonical
state_projection block — {run_state, jobs:[{id,state}]} — alongside its
existing rich .run object (with .run.state) and .jobs[] list (which keep
attempt/role_id). The projection is additive and never replaces those keys;
it is the same lowest-common-denominator shape dashboard and status emit. See
Operator read-surface state projection.
Operator read-surface state projection
run.summary, dashboard --once, and status --json all emit one additive,
strictly identical state_projection block so a script or AFK agent reads
run/job state uniformly across the three verbs instead of special-casing each
(RFC 0157, D251). The pinned shape is:
{ "run_state": "<string|null>", "jobs": [ { "id": "<workflow_job_id>", "state": "<string>" } ] }
jobs[] carries the minimal id (the stable workflow_job_id) and state
pair only — richer per-job fields (attempt, role_id) remain on
run.summary's own .jobs[]. The projection is meaningful only for a single
run: when the verb is invoked repo-wide (status/dashboard with no single
run_id and no resolvable run), run_state is null and jobs is empty.
Every existing key on every surface is untouched (run.summary keeps .run and
its rich .jobs[]; dashboard keeps .jobs_by_state; status keeps .runs[]
and .jobs{}), so existing consumers do not break. Per RFC 0030
an additive result field requires no schema_version bump. dashboard --once
additionally surfaces a top-level state mirroring state_projection.run_state.
Recovery
recovery stale-leases --json applies lazy lease expiry for a run and
reports stale lease recovery context, explicitly distinguishing repo-write
work that requires operator inspection from review-only work that can be
reclaimed safely. recovery requeue-stale --run-id <id> --job-id <id> --json
is a bounded operator mutation for expired non-repo-write work only. It
restores the job's work message to pending when needed, reports when the
work was already reclaimable, and refuses repo-write jobs so abandoned write
work still requires operator inspection or a future worktree-isolated recovery
path.
recovery resume --blocker-id <id> --json resolves an open process-adapter
blocker after the operator has remediated missing outputs. It revalidates
required artifacts, extends the preserved process-adapter lease, marks the
blocker resolved, and returns the job to running. Review jobs whose only
remaining gap is the verdict can then use the normal verdict --verdict accept_with_findings path. --complete --session-id <id> additionally
completes remediated non-review work after validation; nonzero-exit and
timeout blockers require --force.
recovery resume also accepts write-scope blocker kinds
write_scope.out_of_scope_dirty and write_scope_guard_conflict. Because
work.block releases the job lease when such blockers are recorded, recovery
does not complete them inline. It re-runs the write-scope cleanliness check,
resolves the blocker only if the dirty path has been cleared, and requeues the
same attempt for a fresh claim/complete cycle.
Write-scope drift is an attempt-scope recovery condition, not a request to
rewrite historical scope. When artifact.publish or work.complete detects
that the attempt's frozen scope no longer matches the current effective
workflow/context scope, both surfaces return compatible typed failures that name
the recovery path. The recovery path must audit the override or remediation and
may create a fresh attempt or requeue the blocked attempt against its frozen
scope; it must not silently edit the old work packet, artifact rows, or
attempt-scope snapshot.
recovery auto --run-id <id> (RFC 0020 V1) is a one-shot autonomous
sweeper composable with cron / systemd timer. In daemon RPC the
canonical method is recovery.sweep. The sweep first evaluates
recovery.auto_finalize unless the workflow explicitly opts out with
recovery.auto_finalize.enabled=false; live sweep mode never supplies
the standalone auto-finalize --force override. It then runs lazy lease
expiry, optional process reconciliation, optional autonomous review-only requeue
(D036-safe), human-checkpoint timeout escalation, and eligible-blocker
doctor flagging — and returns a structured envelope {run_id, swept_at, run_state, policy_source, dry_run, actions, escalations, still_stuck}. Workflows
declare a recovery_policy block to opt into autonomous behavior.
Audit retention guard (D268 / #599): the auto-finalize circuit breaker is an intentional recovery guard for repeated live auto-finalize failures on the same workflow job. It prevents the recovery loop from retrying a publish/verdict/ finalize failure until the sweeper itself becomes the outage. Deleting the table or status surface requires a fresh evidence pass proving no sweep/status/doctor/ operator path depends on it.
Abandoned-run auto-cancel is the D184 exception to RFC 0020's earlier
auto-cancel deferral: the resident scheduler and explicit recovery.sweep path
may cancel a running run after the default 24h abandonment threshold when there
are no live sessions, no live supervisors or supervised processes, no active
leases, and no progress or durable events in the threshold window. There is no
needs_operator intermediate when all predicates are proven. The predicate
fails closed if any liveness probe is inconclusive or if any active session,
supervisor, process, lease, recent event, unpublished repo-write work evidence,
or other live-work signal exists. The transition records an audit-visible
recovery event and the normal terminal completion record.
Autonomous recovery escalation is represented by daemon state plus
blocker/escalation artifact projections. When a live sweep creates recovery
escalation rows — including the poison-run sweep-breaker trip latch — or an
escalation-class work.block creates a pending escalation inbox row, and
STRIATUM_ESCALATION_NOTIFY_URL is set, the daemon sends a
best-effort post-commit JSON HTTP(S) notification to loopback or tailnet targets
only. Dry-runs do not notify, and notification success or failure is
not authoritative workflow state. CLI flags
(--autonomous-review-requeue, --autonomous-process-reconcile,
--max-requeue, --checkpoint-timeout, --eligible-after,
--dry-run) override workflow defaults. Workflows that omit
recovery_policy get diagnostic-only output; today's flow is
preserved as closely as the daemon PG substrate allows.
The Go production-daemon port also runs a resident active-run recovery
scheduler over the same recovery.sweep behavior. That scheduler updates
daemon-owned PostgreSQL scheduler cursors.
recovery auto-publish --run-id <id> [--dry-run] emits the explicit
recovery.auto_publish_stale_artifacts daemon method. It is the
stale-lease auto-publish path for declared on-disk expected artifacts.
The deprecated recovery.auto daemon method is retired; stale direct RPC or
MCP calls audit as method_unknown. Use recovery.sweep for the one-shot
recovery sweep or recovery.auto_publish_stale_artifacts for the explicit
stale-artifact auto-publish path.
recovery watch --run-id <id> (RFC 0020 step 3) is the long-lived
counterpart for operators who want one foreground command instead of
a cron entry. In production it is not a daemon RPC method; the CLI
keeps the sleep loop, pidfile
(.striatum/scratch/recovery-watch-<run_id>.pid), SIGTERM /
SIGINT graceful shutdown, JSONL stream, and final watch_exit
envelope in the foreground process while each iteration calls daemon
RPC recovery.sweep. The sweep envelope's run_state drives the
exit-on-terminal default (--no-exit-on-terminal keeps looping), and
--max-sweeps N caps tests / probes. The same CLI overrides as
recovery auto are accepted. A pidfile collision with an alive watcher
exits 4 with another recovery watch is active (pid <N>); stale
pidfiles (dead PIDs) are overwritten cleanly.
recovery invalidate-job <job_id> <decision_id> (RFC 0118 P1-6,
capability recovery) invalidates a single compromised completed
review job while the run stays running: the job's verdict rows are
superseded under an accepting operator decision scoped to the exact
(session_id, job_id) (broad decisions refused) — the superseded row,
frozen provenance stamp intact, IS the durable invalidation receipt —
and the job reopens at a fresh attempt that must itself pass the
attested admission gate to re-complete. A recovery.job_invalidated
event names the decision, the superseded verdict, and the reopened
attempt.
Self-Contained Agent Skills
Design rationale: RFC 0015.
striatum skills install [--profile {claude_code, codex, agy, generic, all}] [--scope {project, user}] [--namespace <prefix>] [--force] [--dry-run] writes a self-contained agent skill bundle
into the target tree. The bundle teaches a Striatum-aware agent how
to drive the runner without reading the source repo: each rendered
Markdown file lists the relevant daemon MCP methods and CLI compatibility
verbs, the boundary conditions the runner does not enforce (no direct PostgreSQL writes,
no marker files as state, no durable transcript provenance), and copy-pasteable method
examples.
V1.2 ships four profiles plus an all fan-out:
claude_codewrites one SKILL.md per skill under.claude/skills/<namespace>striatum-*/SKILL.md. The five skills arestriatum-workflow(router),striatum-scaffold(repo add / workflow generate / run prepare / run start / branch confirm),striatum-claim-loop(register-session / claim-next / ack / heartbeat / publish-artifact / verdict / submit-review / complete / worktree create / release),striatum-supervise(RFC 0009 supervisor lifecycle), andstriatum-recover(status / why / doctor / recovery / checkpoint / dashboard).codexwrites the same five-skill content as flat files at.codex/agents/<namespace>striatum-*.md, reusing the Claude Code skill bodies verbatim. Manifest at.codex/agents/<namespace>striatum-workflow.manifest.json.agywrites one SKILL.md per skill under.agy/skills/<namespace>striatum-*/SKILL.md, reusing theclaude_codeskill bundle shape and structure via standard imports (agy plugin import claude), eliminating the need for a separate profile template tree.genericwrites a single concatenated guide at<namespace>STRIATUM_AGENT_GUIDE.mdfor any agent CLI without a skill-discovery convention.allfans out across the four real profiles in deterministic order (claude_code, codex, agy, generic) and returns a{"profile": "all", "results": [...]}envelope. Per-profile manifests stay independent; there is no combined "all" manifest.
--scope user rewrites the prefix to the user's home directory so a
developer who works across many target repos installs once. The
default striatum- namespace can be changed with --namespace for
operators with a collision against an existing skill directory.
Each install writes a .manifest.json describing every rendered file
(rendered SHA256 + bundled-template SHA256 + runner version). A second
invocation against an unchanged tree is byte-identical. An on-disk file
whose hash differs from the manifest is refused_modified without
--force; --force overwrites and updates the manifest;
--dry-run prints the plan without writing. When a requested install
finds an older Striatum-generated manifest for the same profile and
namespace in the other known scope (project vs user), it removes
that peer-scope generated pack if manifest hashes still match; with
local operator edits it refuses unless --force is supplied. This keeps
project and user scopes from exposing two generated Striatum generations
to the same agent.
Repository registration and skill installation are separate commands:
run striatum repo add <path> --init first, then
striatum --repo <path> skills install --profile <profile>. There is
no combined init --with-skills surface in the current Go CLI.
striatum doctor checks every installed manifest across all four
profiles in the registered target repository and user scopes:
skills_missing (a recorded file is absent on disk) and
skills_outdated (the manifest's runner version is older than the
running install, or a packaged template's bundled SHA256 differs from
the recorded template_sha256). It also scans loose generated
striatum-* skill headers and warns when their Generated by striatum
version is more than one minor release behind. Findings surface a
recovery_command with the exact striatum skills install invocation
that would clear the condition; the runner never auto-regenerates during
doctor.
The bundle is self-contained by construction: the renderer rejects
external URLs in template output (a unit test enforces no http:// /
https://), templates ship inside the Go binary/release archive, and
each generated file's header records the runner version that produced
it.
Workflow Authoring Tools
The current Go CLI keeps offline workflow authoring deliberately small:
workflow validate, workflow generate, and workflow templates list/show.
The Python-era workflow plan, workflow graph, workflow lint,
workflow upgrade, and workflow templates render-md commands are retired.
Advisory lint is surfaced through workflow validate --json and generator
preview envelopes; hard validation failures and promoted refusals block
run prepare.
run graph --run-id <id> [--format mermaid|json] renders the same graph for
a live run, annotated with current job state. The Mermaid output appends a
classDef palette and per-node class assignments so renderers can highlight
completed (green), running/claimed/acked (blue), blocked/stale_lease/
waiting_human (yellow), failed/canceled (red), queued (grey), and pending
(light grey, default for jobs with no row yet) jobs. The JSON form extends
each node with current_state, attempt, and a latest_verdict block for
review jobs.
workflow generate --shape <shape> [--lane-set <set>] [--workflow-id <id>] [--scaffold-root <path>] [--artifact-root <path>] [--option key=value ...] [--write] writes a starter workflow tree when
--write is supplied and otherwise previews the planned files. The
generated tree includes <scaffold-root>/workflow.json plus role and
prompt stubs and validates cleanly with workflow validate for the
supported catalog shapes. The command refuses to overwrite existing
files.
Local API And MCP Boundary
The legacy local Python API is retired. Current production authority remains the Go daemon RPC/MCP boundary; compatibility clients must route through that boundary and must not become a second state authority.
The production MCP surface is native to the Go striatumd daemon. It serves
loopback HTTP at /mcp, keeps /mcp/sse as an SSE/backcompat alias, publishes
its current endpoint in the daemon runtime directory as mcp-http-endpoint,
and supports initialize, tools/list, and tools/call over MCP JSON-RPC.
tools/list is derived from the daemon method registry and
capability-filtered per bearer token and repository scope. tools/call
dispatches through daemon RPC, so authorization, request logging, audit rows,
and method-denial vocabulary match the Unix-socket daemon path.
The legacy Python stdio MCP wrapper is completely retired. Tests and compatibility harnesses must use the Go-based daemon MCP, the local web UI, or a documented daemon-backed CLI fallback.
Operator-driven production runs use daemon MCP as the mandatory tool surface.
CLI use remains acceptable only when it is daemon-backed or when a documented
bootstrap/admin/debug exception is recorded by the operator. See docs/MCP.md
for the wire shape and tool list.
Local Service
Design rationale: RFC 0012.
The standalone Python striatum serve process is retired. The Go daemon
(striatumd) mounts the local HTTP service on the same loopback listener as
daemon MCP; the current endpoint is written to the daemon runtime directory as
mcp-http-endpoint and includes the /mcp suffix. Web/API clients strip that
suffix for service routes. The listener is loopback-only by default, and the
handler rejects non-loopback Host headers.
The loopback service requires Authorization: Bearer <client-token> using the
runtime token from the daemon runtime directory. It is read-only by default;
mutating HTTP routes require STRIATUM_DAEMON_WEB_ALLOW_MUTATIONS=1 to be set
on the daemon process before startup. A separate opt-in tailnet identity UI
listener (striatumd -web-tailscale or STRIATUM_DAEMON_WEB_TAILSCALE=1)
serves read-only allowed routes from web-ui.sock for tailscale serve; it is
not a public or hosted surface.
Endpoints return the same {ok, data | error} envelope used by Striatum's
daemon clients:
GET /v1/health—{started_at, mode, allow_mutations}. No DB hit.POST /v1/invoke— body{argv: [...]}; daemon-mapped production reads and mutations route through daemon RPC. Returns 405 when the method is mutating and daemon web mutations are disabled.GET /v1/runs— daemonstatus.GET /v1/runs/<id>— daemonstatusscoped to the run.GET /v1/runs/<id>/why?id=<entity>— daemonwhy.GET /v1/runs/<id>/dashboard— daemondashboardDTO.GET /v1/runs/<id>/events— Server-Sent Events stream over daemonrun.eventsin production. Honors?since=<event_id>andLast-Event-IDfor replay. Emits astriatum.run_terminalevent and closes when the run reaches a terminal state.GET /v1/runs/<id>/artifacts— daemonlist.artifacts.GET /v1/artifacts/<id>/raw— raw artifact content.GET /workflow-templatesandGET /workflow-templates/<id>— daemon template-catalog reads.POST /workflows/generate/preview— workflow generator preview.POST /workflows/generate— workflow generator write; requires daemon web mutations.
The daemon-backed service fails closed when daemon RPC authorization or
repository registration is unavailable. Daemon-routed commands are classified
as read-only only when the daemon method contract has required_capability: "read".
Registry-Backed Multi-Repo Coordination
Design rationale: RFC 0028.
striatumd is the supported foreground daemon entry point, and
striatum daemon install is the bootstrap helper that renders and starts the
systemd user service when available. Per D094 / RFC 0043
the daemon is a hard prerequisite for every Striatum CLI verb;
clients route through the daemon RPC envelope under token/capability
checks. The V1 --no-daemon direct-CLI path is retired and parsing
the flag returns the standard argparse "unrecognized arguments"
error. CLI verbs without a reachable daemon refuse with exit code
11 (daemon_unreachable); the stderr message names the socket path
and the platform-specific remediation, and no local database file is opened
or created.
Daemon-global state — registered repositories, clients, capability
grants, metadata-only hash-chained audit rows, audit segment
manifests, scheduler cursors, and daemon metadata — lives in the
daemon-owned PostgreSQL instance (the "daemon DB"). Per-repository
workflow tables — runs, jobs, sessions, queue messages, leases,
work packets, artifacts, verdicts, blockers, command requests,
process executions, events, worktrees, process supervisors, and
supervisor pointers — live in the same Postgres instance under a
repository_id scope. The historical V1 carve-out that kept those
tables in .striatum/retired-local-state is superseded by RFC 0043.
RFC 0033 specifies the daemon-global PostgreSQL substrate: the
daemon connects through STRIATUM_DAEMON_DB_URL,
~/.config/striatum/daemon.toml, or an explicit --postgres-url
client surface. The daemon owns schema migrations and database
roles, but it does not start, stop, install, or upgrade PostgreSQL.
Bundled, embedded, and Dockerized Postgres distributions are
deferred product choices.
Daemon DB migrations are forward-only and daemon-owned. Startup
applies pending migrations and refuses to run when the on-disk
daemon schema is newer than the daemon binary. striatum doctor
reports substrate version, schema version, audit-chain status,
and segment-manifest verification.
Regular runtime migrations after schema version 26 must not carry owner-table
ALTER TABLE or DROP TABLE DDL against striatumd.* tables. Owner-table
shape changes, SECURITY DEFINER function updates, and grant/revoke repair live
in owner/admin bundles or owner-applied helpers so split-role deployments do
not crash-loop when the daemon starts under the runtime role. Historical
migrations through version 26 remain deployed and hash-stable.
The RFC 0033 daemon-global V1 registry and RFC 0043 repo-local SQLite cutover commands are retired operator surfaces. Legacy SQLite databases and their local-state files are completely retired and not supported. RFC 0048 completed in v1.55.0: production mapped verbs are daemon/Postgres-backed and fail closed without the daemon or repository registration. The legacy local-state implementation and golden fixtures are deleted.
Production daemon registry state is PostgreSQL-only and production dispatch
refuses retired registry fallback. Runtime files are overrideable with
STRIATUM_DAEMON_RUNTIME_DIR.
Linux uses XDG runtime locations; macOS uses Caches for runtime files. Windows
daemon support is not claimed in V1.
striatum repo add <path> [--init] registers a target repository.
It authorizes the daemon admin token before recording the repository in
daemon-owned Postgres. If .striatum/ scratch is absent, registration
refuses unless --init is passed; --init creates only operational
scratch and does not create any SQLite or local database files. If a pre-D094
repo-local SQLite source exists, registration refuses and tells the operator
to archive/remove the legacy SQLite file before registering.
The command canonicalizes the repository root, refuses
symlink/path-traversal ambiguity including symlinked parent components,
derives a realpath/inode-based repository identity from the root, and
refuses active path re-occupation by a different identity. repo remove is
idempotent, marks the repository removed, revokes live repo-scoped
capabilities, preserves audit rows, and never reuses repository_id;
re-adding allocates a fresh id.
Every non-health registry-backed request requires a token. striatum daemon start bootstraps one admin token when daemon-owned Postgres has no clients
and writes the local runtime client-token file with 0600 permissions.
Operators should treat runtime-file token storage as degraded compared with
an OS keyring. Plaintext token secrets are not read from environment
variables and are never stored in the registry or audit log. Authorization
uses the closed daemon method capability vocabulary:
read, write, review, claim, apply, admin, recovery, and
surgical_recovery.
Daemon audit rows are metadata-only. They include client id when known,
repository id when scoped, command, authorization result, denial reason
when safe, transport, request id when supplied, exit code when known,
payload hash, and a continuous hash chain across retained rows. Audit
segment manifests record row ranges and hash anchors; closed segment rows
are SQL-guarded against daemon-API updates/deletes, and striatum doctor
checks retained segment manifests against retained rows. Audit does not
contain request bodies, response bodies, artifact text, blocker prose,
model rationales, terminal output, token secrets, salts, or tracebacks.
It is per-machine daemon evidence, not transcript evidence, authorship
proof, human identity proof, model proof, source provenance, or
resistance to a local filesystem writer.
All production workflow reads and mutations route through daemon RPC.
status, doctor, why, run dashboards, run/job/artifact detail DTOs,
SSE event reads, and dashboard --all read daemon/Postgres state under
capability checks. dashboard --all fans out across registered target
repositories and degrades individual bad repositories without treating
repository files or scratch paths as live run truth. Daemon MCP exposes
capability-gated tools derived from the method registry and read-only
resources derived from daemon state. MCP clients must pass an explicit
token parameter; repo-scoped tokens filter resource lists and are denied
when reading or mutating another repository.
RFC 0030 adds the daemon V2 RPC server-side foundation. The envelope is JSON
schema_version: 1 with client-supplied request_id, dotted method,
object params, optional capability_token, and deadline_ms.
Responses echo the envelope version and request id, set ok, and carry
either data or a stable error object. daemon.hello / daemon.welcome
negotiate envelope version and framing; no ordinary route may run before
the handshake. Incompatible envelope or framing is refused with exit code
10 and must not silently downgrade to direct mode.
The method registry is the code source of truth for daemon V2 routes and
publishes a stable methods_etag through daemon.describe. The closed
capability vocabulary for the RFC 0030/0031 foundation is read,
write, review, claim, apply, and admin. Every authorized or
denied RPC request records metadata-only PostgreSQL request/audit
helpers: method, decision, denial reason where safe, transport, request
id, canonical params hash, row hash, and audit-chain linkage. The audit
contract still excludes request/response bodies, transcripts, artifact
contents, token secrets, salts, tracebacks, and model prose.
Post-D094 and D103, ordinary operator commands must use the daemon authority boundary. Direct repo-local dispatch is a development/test harness path, not a production run mode. Any installed CLI path used by an operator must route through the daemon or be documented as a bootstrap/admin/debug exception.
RFC 0031 adds daemon-owned supervision and sealed-apply foundation
state. The daemon DB contains daemon_supervisors and apply_receipts;
process_supervisor_pointers live in the daemon-owned per-repo
PostgreSQL tables under repository_id, so packet delivery,
lane-attestation, and evidence paths read the same live substrate. The
reviewed-patch apply mutation is not part of the production daemon RPC
contract per D112; stale direct calls to apply.reviewed_patch return and
audit as method_unknown. Apply receipt read/verify routes remain as
evidence helpers, not cryptographic non-repudiation claims against a
malicious local operator. The Go daemon can rotate and load the local
Ed25519 0600 fallback signing-key file through daemon.key.rotate; full
reviewed-patch mutation and stronger key custody are still separate
apply-gate work.
The daemon V2 capability vocabulary is read, write, review, claim,
apply, admin, recovery, and surgical_recovery, and each registry
method declares a repository scope mode: single_repo or daemon_global.
Daemon MCP mutation tools are derived from that method registry when the
PostgreSQL daemon substrate is active. tools/list
returns only the effective supported production tool set allowed by the
token's capability, repository scope, and production-support visibility
filter, while tools/call re-authorizes every request even if the tool was
listed earlier. Denials are metadata-audited with transport mcp; hidden
tools are not treated as authorized. There is no V2 daemon MCP equivalent of
the daemon web mutation gate. Per D103, this daemon MCP surface is mandatory for
operator-driven workflow mutation, not an optional convenience wrapper.
Historical RFC 0032 migrations may leave cross_repo_runs,
cross_repo_run_repositories, cross_repo_cycle_counters,
audit_repositories, and runs.cross_repo_run_id in upgraded databases.
After D270, these are historical compatibility schema, not supported
production surface: no current daemon RPC method or CLI route exposes
cross-repo run lifecycle, read, or recovery behavior.
The foreground sweep process uses the existing recovery auto policy
against active registered runs without requiring one recovery watch
process per run. The running process uses internal authority for its
periodic sweep. The manual striatum recovery auto CLI verb routes to the
daemon recovery.sweep method. The sweep does not auto-resolve human checkpoints, requeue
repo-write stale work, or substitute for daemon-owned process supervision.
Each per-run sweep writes a daemon daemon.recovery_sweep event with
payload author: striatumd-<instance-id>; review-only stale requeue
events produced by this path carry the same payload author. Other
underlying recovery event bylines remain direct-recovery semantics and are
deferred to a follow-up RFC. The first sweep iteration is in registration
order; later iterations order repositories by last sweep time where cursor
data is available. Per-run ordering inside one repository remains
runs.created_at order. A per-run timeout marks the scheduler cursor
sweep_degraded, and striatum doctor surfaces degraded cursors and an
active recovery watch pidfile for the same registered run as duplicate
recovery scheduling.
Audit segment append-only manifests are implemented, but production retention/rotation policy is deferred; the active registry can grow until an operator or future RFC supplies rotation/export behavior.
Production supervision metadata, lane attestation inputs, and supervisor
pointers live in daemon-owned PostgreSQL under the registered repository
scope. supervise.* CLI calls are daemon clients, not direct repo-local
state edits. Legacy repo-local supervision shapes and one-shot wrappers are completely
retired. Supervision state and delivery are daemon-owned; supervised lane
processes default to the daemon OS user, or launch as the configured
STRIATUM_LANE_OS_USER through noninteractive sudo when the host adopts the
PG-less lane-user profile.
Supervisor table split guard (D266 / #608): the current three-table split is intentional until an equivalence-tested collapse plan exists.
| Table | Owned invariant |
|---|---|
process_supervisors | Repo-scoped supervised lane record: session/run binding, adapter command, scratch/FIFO paths, pid, state, and heartbeat. |
process_supervisor_pointers | Mutable run/session pointer and delivery/liveness metadata used by attestation, packet delivery, recovery probes, and heartbeat coalescing. |
daemon_supervisors | Daemon-instance launch envelope: command hash, daemon instance id, cwd, stdin pipe, pid identity, and daemon-side custody. |
The split must not be collapsed as a paperwork cleanup. Collapse requires stop/reap, heartbeat/progress, lane-attestation, and recovery-liveness equivalence tests.
Before a supported Codex agent-loop lane is launched, supervise.start also
applies the RFC 0121 lane provider-auth gate. The provider_auth_gate mode is
auto by default, required to fail unsupported providers, and off only as
an explicit rollback. The gate runs after frozen workflow/lane/run-as
resolution and before supervisor scratch/FIFO creation, session-bound lane
token minting/injection, supervisor rows/events, helper/tmux setup, or the real
provider process. It returns only safe classification fields such as probe
name, exit code, stdout/stderr byte counts, and bounded success-signal state;
raw provider stdout/stderr/final text, auth paths, provider account ids,
environment values, token material, PTY logs, and tracebacks are never returned
or persisted. For Codex, a zero-exit smoke is provider-auth success; a missing,
empty, or mismatched bounded --output-last-message signal is exposed as a
diagnostic and does not block lane launch.
The Python daemon is no longer a selectable production core. RFC 0039
introduced go/cmd/striatumd behind the RFC 0030 envelope-v1 wire protocol
and RFC 0033 PostgreSQL substrate. RFC 0068 and D111 make that Go daemon the
only supported daemon implementation; lifecycle is managed by striatumd
directly or by the unit installed through striatum daemon install. Current Go handler
coverage has no missing or generic
not_implemented active contract methods. D110 deliberately removed the
SQLite-bound
daemon.migrate_repo_local, dogfood.publish_on_behalf, and
dogfood.surgical_recovery RPC names from production discovery and the daemon
method contract. D112 likewise removed apply.reviewed_patch from the
production daemon RPC contract; stale direct calls audit as method_unknown.
D113 closes the writable import window. The Python daemon module, Python MCP
wrapper, legacy local-state package, root compatibility facades, direct corpus
exporter, V1 local-state schema module, deterministic repo-local fixture, and
all Python CLI/web/source/test surfaces have been completely retired and deleted per RFC 0078.
Multi-Principal Trust Model
Design rationale: RFC 0107 (multi-principal trust model); builds on RFC 0028 (per-repository scoping), RFC 0053 (human principal / operator roles), and RFC 0096 (session-bound capability tokens).
Striatum is self-hosted on a laptop or a server, serves multiple repositories, and may serve multiple human users — and is explicitly not a hosted, multi-tenant SaaS control plane. The multi-principal model makes multi-user a deliberate, bounded design over the existing trust substrate, not an emergent accident.
A principal is a named identity that holds capability tokens. A principal
has a kind:
human— a human operator. This generalizes RFC 0053's single escalation-only human principal to several humans, each with their own identity, capability grants, and audit attribution.ai_operator— an autonomous operator identity that scaffolds and drives runs.service— a non-interactive automation/daemon client.
Principals sit above the existing clients / client_capabilities
substrate; the model adds no new wire RPC method and reuses the existing
daemon.token.* admin verbs:
- A principal owns one or more clients. A
client(with its per-capability, per-repository, optionally session-bound grants inclient_capabilities) is the wire credential. Because token rotation mints a new client row, the principal→client link is what keeps a principal's identity stable across rotation. - The mapping is recorded in two daemon-global tables introduced by the RFC
0107 migration:
striatumd.principals(the identity) andstriatumd.principal_clients(the active client links). A client is actively linked to at most one principal, so audit attribution is unambiguous.principal_clients.client_idis a bare column with no foreign key into the owner-heldclientstable — referential integrity is enforced in the daemon, exactly asaudit_log.client_idalready is — which keeps the migration ownership-safe (it only creates new tables; it never alters an owner table). daemon.token.createaccepts an optionalprincipal_id(withprincipal_kind+principal_display_nameto create the principal on first use) and attributes the minted client to that principal in the same transaction that issues the token.daemon.token.rotatecarries the rotated-from client's principal onto the replacement client, so rotation preserves a stable principal identity.daemon.token.revokerevokes the client's capabilities but leaves its historical principal link intact so past audit rows remain attributable.
Capability + repository scoping. Per-principal scoping reuses RFC 0028's
client_capabilities.repository_id and RFC 0096 session-binding unchanged:
- A principal may hold capabilities on a subset of repositories; the
authorizer denies a token acting on a repository it was not granted with
capability_scope_mismatch. No principal can read or mutate another repo's live state without an explicit grant. - A session-bound token (minted for a single lane, held by exactly one
principal) may act only as its own session; principal A's token cannot
act for principal B's session. This is the existing
enforceSessionBindingrule, stated canonically asAuthContext.MayActAsSession.
Audit + attribution. The daemon-global hash-chained audit log records the
acting client_id on every mutation (RFC 0030/0033); resolving that client to
its principal via principal_clients attributes every mutation to a principal.
Durable Markdown artifacts continue to carry the lowercase privacy-safe byline
author: <role-name>-<model-name>-<ordinal> (RFC 0026/0090); the principal
linkage is the control-plane attribution that complements that on-disk
provenance.
Inspection. striatum doctor surfaces a principals block listing each
configured principal with its kind, display name, active client count, granted
repositories, and effective capability scope (per capability, per repository,
and whether session-bound). The block never reads or returns token material.
The boundary that keeps it non-SaaS. Principals are local trust grants on the operator-owned daemon + PostgreSQL, not cloud accounts. This model introduces no hosted control plane, no tenant-provisioning service, no external identity-provider or SSO dependency, and no telemetry. Loopback/tailnet access (RFC 0085) is the network access model; principal identity is the local capability-grant dimension over it. Adding any hosted/tenanted surface remains out of scope and would require an explicit, separate product decision.
Local Web UI
Design rationale: RFC 0013 (V1 surface
- JSON API + SSE feed); RFC 0022 (V1 server-rendered redesign + SVG dependency graph).
striatumd activates the bundled Go web service on the daemon's loopback HTTP
listener; there is no striatum serve --web command. Loopback requests require
the runtime bearer token. The optional RFC 0085 tailnet listener serves a
read-only browser surface from web-ui.sock behind tailscale serve.
Current Go routes:
GET /andGET /run?run_id=<id>→ server-rendered status pages backed by daemonstatus.GET /static/<path>→ bundled static asset.GET /v1/health→ service health plusallow_mutations.GET /v1/runs,GET /v1/runs/<id>,GET /v1/runs/<id>/why,GET /v1/runs/<id>/dashboard,GET /v1/runs/<id>/events, andGET /v1/runs/<id>/artifacts→ daemon-backed JSON/SSE reads.GET /v1/artifacts/<artifact_id>/raw→ raw artifact bytes.GET /workflow-templatesandGET /workflow-templates/<id>→ bundled workflow template reads.POST /v1/invoke,POST /workflows/generate/preview, andPOST /workflows/generate→ daemon-backed mutation-capable endpoints; mutating calls fail closed unlessSTRIATUM_DAEMON_WEB_ALLOW_MUTATIONS=1was set on the daemon process before startup.
The richer Python-era pages (/run/<id>/job/<id>,
/run/<id>/artifact/<id>, /doctor, /view, /workflows/new,
/workflows/edit, /chat) are retired unless a future Go route reintroduces
and documents them.
Adapter Boundary
The minimum integration contract is process-based: command array, cwd, env, stdin, stdout, stderr, exit code, and optional PTY/tmux wrapping. Provider features live in lane command configuration. Core scheduling does not parse terminal output or infer behavior from provider names.
Adapter constraint enforcement has four levels: enforced (the adapter
prevents the constraint from being violated), advisory_strict (the adapter
takes best-effort steps the agent cannot easily undo, such as scrubbing proxy
env vars or setting STRIATUM_NETWORK_POLICY / STRIATUM_REPO_SCOPE
sentinels), advisory (the constraint is recorded and surfaced but not
mechanically restricted), and unsupported (the adapter cannot represent
the constraint). Workflow validation rejects a lane whose required_enforcement
asks for a stronger level than the adapter can provide.
adapter run is the remaining single-shot process-adapter compatibility
path. It launches the configured process lane command for an active
claimed lease, can pass the stored work packet on stdin, sets
STRIATUM_* environment variables, creates a
.striatum/scratch/<process_id> scratch directory, and records process
metadata plus lifecycle events through the legacy compatibility schema.
The daemon-owned supervised-session path is the production long-lived
process path. Single-shot stdout and stderr are suppressed unless the
operator explicitly requests inherited stdio; Striatum does not store
single-shot transcripts in daemon state or durable artifacts.
The process adapter graduates network=forbidden and
repo_scope=local_only to advisory_strict; transcript-off is enforced.
Process Supervision
adapter run is single-shot: the child exits with the configured command,
and the next work packet must spawn a fresh process. Long-lived agent CLIs
(Codex, Claude Code, agy, etc.) need a different shape: one
persistent process that receives multiple work packets across multiple
turns. RFC 0009 introduces a striatum supervise command group plus a new
process_supervisors table for that flow. The two adapter modes coexist;
adapter run is unchanged.
Single-Shot Process Adapter Completion Guarantees (RFC 0014 V1)
After every adapter run exit (including timeout-fired SIGTERMs), the
runner inspects required expected_artifacts and, for type: "review"
jobs, whether a verdict was recorded. When any required output is
missing — or the child exited non-zero or hit the timeout — the job
transitions from running to blocked, a blocker row is inserted, and
a privacy-safe diagnostic envelope is recorded as the new
blockers.payload_json column.
--timeout-seconds <n> on adapter run wraps process.communicate
with a deadline; on expiry the child is SIGTERM'd, then SIGKILL'd
after a 5-second wait. lanes.<id>.adapter_timeout_seconds provides
a per-lane default (capped at 86400 / 24 hours by workflow validation);
the CLI flag overrides the lane field; with neither set, behaviour
stays unbounded for backwards compatibility.
Blocker reasons (blockers.blocker_kind):
process_outputs_missing— exit0, required artifact(s) missing.process_review_verdict_missing— exit0, review job without a recorded verdict.process_exit_nonzero— non-zero exit (priority over output checks).process_timeout_exceeded—--timeout-secondsfired.process_lost_with_outputs_missing— reconciler found a dead PID whose job had missing required outputs.
striatum recovery process-reconcile --run-id <id> walks
process_executions.state = 'running' rows; for each, os.kill(pid, 0)
checks liveness. Externally-killed rows transition to 'lost' and
re-run the same output validation; the JSON output mirrors the
existing recovery requeue-stale shape (D036's lazy-on-CLI policy).
Two doctor checks surface bookkeeping mismatches:
process_running_but_pid_gone and
process_running_with_expired_lease. striatum status --run-id
gains a process_health summary key.
The diagnostic envelope contains zero child stdout/stderr (D028 preserved by construction); it carries only metadata Striatum already collected plus output-validation deltas:
{
"envelope_version": "striatum.process_adapter.envelope.v1",
"process_id": "proc_<hex>",
"command": [],
"exit_code": 0,
"duration_seconds": 0.0,
"timeout_seconds": null,
"missing_artifact_paths": [],
"review_verdict_missing": false,
"recovery_commands": []
}
process_supervisors is added by migration version 4 and is separate from
process_executions so single-shot launches and supervised sessions keep
distinct rows and event streams. State values are
('starting','attached','detached','lost','stopped') and a partial unique
index on session_id enforces "at most one active supervisor per session"
without blocking historical stopped or lost rows.
The supervise CLI surface:
striatum supervise start --session-id <id> [--provider-auth-gate auto|required|off]validates the session is active and that its lane uses theprocessadapter, refuses if the session already has a supervisor in('starting','attached','detached')state, applies the lane provider-auth gate for supported Codex agent-loop lanes, and forks the lane command in a daemon-owned persistent supervision session only after the gate passes or is explicitly off. WhenSTRIATUM_LANE_OS_USERnames a distinct OS user, the daemon launches the lane command and any tmux session as that user withsudo -n -u <lane-user> -- env -i ...; otherwise it preserves the same-user behavior. It keeps raw provider output out of daemon/PostgreSQL state and durable artifacts, and transitions the row toattachedonce the child pid is alive. Asupervisor.startingandsupervisor.startedevent are recorded. Agent-loop lanes recordagent_loop_mode: "self_driving"and receive work throughwork.await_packet. Non-agent-loop supervised lanes recordagent_loop_mode: "supervised_push"; after attach,supervise startattempts one atomic auto-dispatch that claims the next eligible packet for the session and delivers it to the supervisor FIFO, returning anauto_dispatchsummary (delivered,no_work, orfailed).striatum supervise send --session-id <id> --packet-id <id>delivers the work packet prompt directly to the daemon-owned interactive PTY master (stdin-submit) using the per-adapter submit key-sequence (Enter /\r), refreshesheartbeat_at, and records asupervisor.packet_deliveredevent. The helper-facing delivery FIFO is opened in nonblocking mode. If the helper or stdin reader is gone, delivery liveness is marked degraded with reasonstdin_reader_missing, the call fails with a structured refusal, and nosupervisor.packet_deliveredevent is recorded. Delivery degradation is stored undertmux.delivery_livenessfor tmux-shaped supervisors and under top-leveldelivery_livenessfor no-tmux/plain supervisor metadata; send guards and read projections must honor both shapes. A transienttmux_unavailableprobe stores a degraded tmux liveness record and refuses delivery without marking the lane lost until the consecutive unavailable threshold is reached. Agent-loop lanes default to persistent PTY-helper supervision with preserved context. The oldersupervision.stdin_delivery: "one_shot_eof"named-pipe transport remains an explicit compatibility path for lanes that cannot yet run an agent loop. State reactions are daemon-driven through MCP/RPC methods (artifact.publish,work.ack,work.complete,review.verdict), with CLI compatibility fallbacks available for older workflows. The supervisor never parses agent stdout.- PTY-helper lanes can set
supervision.require_tmux: trueto fail closed iftmuxis unavailable or run/lane metadata is missing. Without that opt-in, PTY-helper launch may fall back to a plain PTY and records tmux unavailability as metadata. Agent-loop lanes default to the PTY-helper transport so they are tmux-backed and operator-attachable when tmux is available; a workflow may explicitly setsupervision.transport: "pipe"for a lane that still requires legacy pipe delivery. - Tmux-backed PTY-helper launches treat the tmux pane process as the supervised
lane identity. Launch creates a placeholder pane, configures
remain-on-exit, and then respawns the lane command so immediate startup exits retain a dead pane for diagnostics. Tmux setup and cleanup commands are bounded by a setup timeout. Tmux session names are bounded for tmux compatibility and include a stable hash suffix over the full run/lane/ supervisor identity so truncation cannot collide across supervisors. The pointer metadatatmuxblock carriesstate: "backed",session_name,window_id,pane_id,pane_pid, optionalpane_start_token,attach_command, diagnosticattach_client_pid,captured_at, and probe visibility fields such aslast_ok_at,probe_skipped_at,probe_unavailable_count,last_unavailable_detail,liveness_state, optionalrun_as_user, andliveness. Tmux liveness payloads includestate: healthy|degraded|lost; failures also include a typedprobe_failurerecord withfailure_class, optionalexit_code, optionalerrno, optionalpane_process_alive, and optionalobserved_pane_pid.tmux attach-sessionis an observer and packet-delivery handle only; its PID is not the lane liveness identity. - Helper-local teardown paths, including context cancellation and
packet-forward failure, send
tmux kill-session -t <session>for tmux-backed lanes before considering any direct pane-PID cleanup. A direct pane-PID signal is permitted only when the stored pane start token is numeric and still matches the current process; missing, literal, unavailable, or mismatched tokens skip direct signalling. striatum supervise stop --session-id <id> --reason <text>sendstmux kill-session -t <session>for tmux-backed lanes, then cleans up the helper process metadata without signalling the diagnostic attach-client pid. Any direct PID cleanup fallback, including helper PID cleanup and tmux unavailable pane cleanup, is gated by the recorded pid start token; stale, missing, or unverifiable tokens skip the signal and annotate thesupervisor.stoppedevent. Plain PTY lanes keep the existing SIGTERM then SIGKILL path only when the recorded start token still matches. The row is markedstoppedandsupervisor.stoppedis recorded.striatum supervise rebridge --session-id <id>re-attaches the helper-owned tmux delivery bridge in place for an attached tmux-backed supervisor. It first performs the same tmux session/pane liveness probe used by status and send. The command is valid only when the recorded pane process is still live;tmux_pane_dead, missing/mismatched pane identity, or unverifiabletmux_unavailableprobe results fail closed with remediation guidance. Rebridge recreates the per-supervisor stdin FIFO when missing, starts a fresh helper attach path, updates helper/attach-client pointer metadata, clears delivery degradation, and recordssupervisor.rebridged. It does not kill, reset, or respawn the tmux pane.striatum supervise status --session-id <id>probes tmux-backed liveness withtmux has-sessionplus a pane identity query (pane_id,pane_pid,pane_dead,pane_start_time). Plain PTY rows continue to use PID/start-token liveness. Tmux failure classes aretmux_session_missing,tmux_pane_missing,tmux_pane_dead,tmux_pane_pid_mismatch, andtmux_unavailable; the successful class istmux_ok. The status projection exposes these undertmux.livenessand never reads pane text. A tmux lane whose pane is live but whose start token cannot be verified remains operationally live but projectslane_attestation: "unattested"withlane_attestation_reason: "start_token_unverified". Only numericpane_start_time/start-token values count as verified identity evidence; if tmux cannot report a numeric livepane_start_time, the probe may compare the recorded numeric pane start token against the OS process start token for the observed pane PID. Literal tmux format strings or other non-numeric values are treated as unverified. An attached row whose lane is alive but whose active lease has stale progress returnsliveness: "stalled"pluslast_progress_at,last_progress_age_seconds, active lease metadata, andstall_after_seconds. Status also exposeslane_backend(tmux,plain_pty_fallback, orplain_pty),delivery_state,pane_liveness,trajectory_log, and failure-class-derived remediation hints.trajectory_logreports whether the operator-local PTY diagnostic log is expected, available, missing, or unreadable and gives its path/size when known. Status itself never starts or kills processes and never includes terminal log content.striatum supervise list --run-id <id> [--state <state>]lists rows for a run, optionally filtered by state; each row includes the samelane_backendandtrajectory_logmetadata.striatum supervise trajectory --session-id <id> [--tail | --tail-lines N]reads the latest supervisor's operator-local PTY diagnostic log from.striatum/scratch/<supervisor_id>/pty.log. Without a tail flag it returns the full local file;--tailreturns the last 200 lines. This is an explicit operator diagnostic read, not a workflow-state projection.- Daemon RPC
supervise.reattach_statusreturns a read-only supervisor health DTO for a run/session/supervisor filter. It compares repo supervisor rows, daemon supervisor pointers, daemon supervisor rows, PID/tmux liveness, and PID or pane start-time identity, classifying each row asreattachable,lost_candidate,needs_repair,needs_verification, orterminal. It does not mutate state; actual restart/lost-state transitions remain daemon lifecycle work. In-place delivery repair is handled bysupervise.rebridgeand is deliberately limited to live tmux panes; terminal tmux liveness failures still require stopping the broken supervisor and starting/reclaiming a replacement lane through daemon workflow controls.
Recovery: before ordinary stale-lease handling, recovery.sweep evaluates
attached supervisors with active claimed/running work. A stale-but-unexpired
heartbeat emits supervisor.heartbeat_stall once per lease/supervisor so
doctor, why, and status surfaces show the lane as suspect.
The autonomous decision tree additionally scans queued/claimable jobs whose
bound supervised session is hung (D201, #291): a never-claimed (leaseless)
queued job is bound to a supervised session by the same role+lane eligibility
the claim path uses (only when exactly one such still-active, non-terminal-pointer
session matches), and when that bound session is honestly stalled past its idle
deadline or its agent process/pane is confirmed dead, the sweep closes the hung
owning session so a fresh lane can claim the already-pending job. The dashboard
supervisor_stalls projection surfaces the same leaseless hung session as a
stall once its heartbeat ages past the stall threshold (a leaseless_count
counter), instead of reporting stalled_count:0 while the run sits wedged. When the
same attached supervisor's active lease has expired, sweep opens a
heartbeat_stall_lease_expired blocker, transitions the job/message to
blocked, expires the lease with release_reason='heartbeat_stall', marks
the supervisor row/pointer lost, and records supervisor.heartbeat_stall,
lease.expired, job.blocked, and
supervisor.lease_expired_with_supervisor. The OS process is not
auto-killed; operator inspection is required, mirroring D036's stale-lease
policy for repo-write work.
The Go striatum-supervisor-helper is a narrow process/PTY helper. It emits
newline-delimited control events with schema
striatum.supervisor_helper.event.v1: agent_started, packet_accepted,
progress, artifact_observed, helper_error, attach_client_exited, and
agent_exited. attach_client_exited means the tmux attach observer exited
while the helper's pane probe still showed the lane alive or unverifiable. The
daemon treats that helper-reported liveness as advisory and performs its own
fresh tmux session/pane probe before deciding whether to keep the supervisor
attached or move it to detached. When the daemon-observed pane is live and the
attach observer was the helper-owned delivery bridge, the daemon keeps pane
liveness attached/attested, records tmux.attach_client_last_exit, and marks
delivery_liveness degraded with reason attach_client_exited; later
supervise.send calls refuse that supervisor until supervise.rebridge,
restart, or a future alternate delivery path clears the degradation. Non-tmux
or unproven attach exits still move the supervisor to detached instead of
treating the lane as lost.
Daemon supervise.report can consume those helper events as JSONL text, a
path, or an object list and records lifecycle events through the same durable
supervisor.<event> event path used by wrapper reports. progress is the
exception: it is liveness chatter, not durable provenance, and is not appended
as supervisor.progress or stored in a separate off-chain table. A meaningful
progress report still refreshes supervisor heartbeat state, stamps session PTY
liveness, and, when the session owns an active lease, refreshes that lease; the
derived lease.heartbeat event remains the durable record of the lease-state
transition. Helper timestamps are preserved as reported_at on chained helper
events; agent_exited applies the normal stopped-state transition. The control
channel carries lifecycle metadata and byte counts, not model transcript
output.
Doctor: striatum doctor flags supervisors in ('starting','attached', 'detached') whose pid is gone, and attached supervisors whose
stdin_pipe_path no longer exists on disk. It also surfaces
supervisor_lost_with_held_lease (HARNESS-001) when a supervisor row
is in state lost while the session still owns an unexpired active
lease — the symptom that the supervisor exited before the work
completed and the run is silently stuck. striatum status adds the
stable next-action recover_orphan_supervisor for the same condition
so dashboards and scripts react before the lease default expiry (30
minutes) is hit. In daemon/Pg mode, doctor also surfaces non-healthy
supervise.reattach_status states (lost_candidate, needs_repair,
and needs_verification) so stale supervisor repair is visible before
a mutating recovery path runs. It also reports
supervisor_attached_stale_heartbeat and
supervisor_heartbeat_stall_lease_expired for attached supervisors whose
control-plane progress is stale. striatum supervise stop is idempotent
against a supervisor whose latest row is already lost or stopped:
rather than raising InvalidTransitionError, it returns the existing
terminal row plus a note describing the prior state.
striatum doctor --lane-provider-auth codex --json is an explicit diagnostic
over the same provider-auth primitive; ordinary doctor and
doctor --verbose never invoke provider CLIs.
Supervised Lane Command Contract
The lanes.<id>.command array configured for a process-adapter lane
is the program Striatum forks under supervise start. To work with
the supervised flow, that command must satisfy three requirements
(absent any of them, supervise start happens, but the run silently
fails to advance and doctor surfaces
supervisor_lost_with_held_lease):
- Persistent PTY Interactive Model. All process lanes run natively in
persistent interactive PTY sessions owned by the daemon. One-shot per-turn
wrappers and
--printconfigurations are retired for live agent-loop lanes. A lane invokingclaudewith--printor-pis hard-refused byworkflow validate,run prepare, andsupervise startunless the lane explicitly declaresallow_claude_print: true. The override is reserved for genuine compatibility fixtures; normal Claude lanes should use an interactive command such as["claude", "--dangerously-skip-permissions"]withadapter_capabilities.agent_loop: true. - Submit Prompts via stdin-submit. Per-turn prompt payloads are delivered
directly to the PTY master of the long-lived process using the per-adapter
submit key-sequence (Enter /
\r), preserving agent context across turns. - Call back through daemon MCP/RPC. The agent advances workflow state by
invoking
work.ack,work.heartbeat,artifact.publish,work.block,review.verdict/review.submit, andwork.completewith the identifiers from the packet. CLI commands for the same methods remain compatibility fallbacks. The agent's only durable output is the artifacts and verdicts it records through the daemon.
Operator-Local PTY Logs
Agent-loop PTY lanes may tee the provider terminal stream to an operator-local
diagnostic file under .striatum/scratch/<supervisor_id>/pty.log (created
0600). The path can be overridden with
STRIATUM_AGENT_LOOP_DEBUG_LOG=<path> or disabled with
STRIATUM_AGENT_LOOP_DEBUG_LOG=off / /dev/null.
striatum supervise status and striatum doctor report the log metadata
(trajectory_log.status, path, size, and whether an agent-loop log is
expected). striatum supervise trajectory --session-id <id> [--tail | --tail-lines N] is the first-class read command for the file contents.
These PTY logs are operational scratch, not transcript provenance. They are
not stored in daemon-owned PostgreSQL, not published through
publish-artifact, not included in evidence exports, corpus exports, or run
archives, not parsed for workflow state, and not used for byline attestation,
verdicts, completion, or recovery decisions. They may contain provider output,
tool text, prompts, or secrets visible in the terminal, so operators must treat
them as private diagnostics and never commit them. Deleting them does not
change workflow truth.
A working supervised lane therefore needs an agent that knows the
Striatum protocol — a project skill, an embedded loop, or a wrapper
script — not just a raw model invocation. The shipped wrappers live at
.striatum/bin/{claude,codex,agy}-supervised-wrapper.sh (where agy
profile reuses claude template structures under .agy/). Each
wrapper maintains the long-lived process loop, logs provider stdout/stderr under
STRIATUM_SCRATCH_DIR, and traps SIGTERM to clean up the in-flight inner process.
Go tests under go/pkg/agentloop/ verify those loop semantics
with provider-command stubs so they do not depend on real agent
binaries. dogfood-001's HARNESS-001 captured the "default scaffold
ships a non-viable lane command" foot-gun; this contract is the
explicit form of what that proposal asked the runner to require.
Worktree Isolation
Lanes may opt into per-job filesystem isolation by setting
worktree_isolation: "per_job". The default is "off", which keeps current
single-worktree behavior for plain operator-by-hand workflows. Supervised or
agent-loop lanes that perform repo-write work must use per_job isolation
before workflow validate, run prepare, or run start will accept them,
unless the lane records the explicit interactive-human compatibility override:
allow_shared_checkout_repo_write: true plus a non-empty
shared_checkout_repo_write_rationale. That override is for compatibility
workflows only and still leaves a lint warning. When a lane is configured for
per_job isolation, work packets for repo-write jobs in that lane include
worktree_required: true and a commands.worktree_create invocation. The
runner does not auto-create worktrees on claim; the agent must call
striatum worktree create itself.
striatum worktree create --session-id ... --job-id ... --lease-id ...
validates the active lease, requires the lane to declare per_job isolation,
requires the job to be repo-write, and rejects requests when an active
worktree already exists for the job. It runs
git worktree add --detach .striatum/worktrees/<worktree_id> <base_branch>
based on the run's confirmed branch and records a row in the
job_worktrees table with state active. If a pre-RFC 0117 run recorded a
confirmed branch name but the git ref is missing, worktree creation creates
the branch ref at the run's recorded base using ref plumbing; it never moves
the operator's primary checkout.
When work.complete completes a repo-write job with an active per-job
worktree, the daemon first makes that worktree HEAD reachable from a durable
git ref. It fast-forwards the run branch with compare-and-swap update-ref
when the run branch is an ancestor of the worktree HEAD; otherwise it pins
the worktree HEAD at refs/striatum/<run_id>/<job_id>. The completion emits
job.commits_anchored with the anchor kind.
striatum worktree release --worktree-id <id> refuses to remove a worktree
whose HEAD is not reachable from the run branch or a refs/striatum/ pin,
returning worktree_head_unreachable. Passing --force performs the
discarding removal and emits worktree.force_released. If the path is already
missing on disk, --force may retire the row only when the owning job is
terminal; the event payload records missing_on_disk: true. Otherwise a
reachable release runs git worktree remove --force, emits worktree.released,
and marks the row removed. Releasing an already-terminal row is a no-op.
striatum worktree list [--run-id <id>]
returns the rows plus each job's workflow_job_id and a read-only ref-safety
projection: head, reachable, anchor (run_branch, job_pin, none, or
unreachable), anchored_ref, and checked_refs.
striatum worktree gc [--run-id <id>] removes on-disk worktree directories for
terminal jobs whose HEAD is already reachable from the run branch or a
refs/striatum/ pin. It also retires terminal rows whose path is already
missing on disk and records missing_on_disk: true. It skips non-terminal jobs,
probe failures, and unreachable HEADs, returns skipped rows with typed reasons,
marks removed rows removed, and emits worktree.gc_removed.
publish-artifact continues to validate write scope and content against the
logical repo-relative path, but when an active worktree exists for the job it
reads the file from <worktree_path>/<logical_path> and records the
artifact's repo_path as the logical path. Artifacts remain durable
provenance for the main branch regardless of which worktree the work
happened in.
For repo-write jobs whose lane declares worktree_isolation: per_job, the
repository-touching surfaces artifact.publish, repo.write,
repo.patch-preview, repo.patch-apply, process.run, and work.complete
require an active job worktree and refuse with worktree_required if the
agent has not called worktree.create.
Lazy lease expiry preserves the worktree directory for operator inspection.
Before the job_worktrees row is marked abandoned, recovery anchors the
worktree HEAD through the same fast-forward-or-pin helper and records the
anchor in worktree.abandoned; git worktree remove is not run.
striatum doctor flags active worktrees whose lease is no longer active and
active worktrees whose path no longer exists on disk. It also reports
worktree_ref_safety and flags worktree_head_unreachable when an active or
abandoned worktree HEAD is not reachable from the run branch or a
refs/striatum/ pin; completed jobs in that state also surface
job_completed_without_anchor.
First Validation Fixture
The first fixture is RFC-ledger cleanup:
draft -> parallel reviews -> findings ledger -> synthesis -> final review
Tests exercise it with fake sessions and no live model calls.
A smaller generic docs-only workflow fixture also lives at
examples/docs-review-flow/workflow.json. It covers draft, review, and apply
steps without Engram-specific paths or live model requirements.
Verification
The required check is:
make test
The contributor smoke sequence is script-owned:
scripts/package_smoke.sh
scripts/fresh_clone_smoke.sh
Both scripts use daemon-owned PostgreSQL and the Go daemon when PostgreSQL setup is available; if it is unavailable they skip with a clear message instead of entering a retired local-state fallback.