Summary
An artifact_exists gate can never pass for a file under a dot-directory, because the worker's journaled artifacts list omits those paths entirely. The gate is correct by its own rules — it reads the journal — but the journal does not describe what the agent actually wrote.
.workflow-artifacts/ is the conventional artifact directory in AgentWorkforce/relay (every flow spec in that repo writes there), so in practice artifact_exists is unusable for the thing it exists to check.
Evidence
Run 01M2ZQP6P9ME3PRBWDXQA32MT3, CLI 2.0.22, macOS arm64, flows run --local-agent.
The run died at shadow-rust.gate with the file it was gating sitting on disk:
FAILED [step_failed] Step "shadow-rust.gate" (deterministic) completionReason: retries_exhausted exit=1.
$ ls -la .workflow-artifacts/migrate-native-delivery/phase-0-seam-20260920c/reviews/shadow-rust.md
-rw-r--r-- 1 khaliqgant staff 25257 Sep 20 08:57 ...
The gate was { type: 'artifact_exists', path: '.workflow-artifacts/migrate-native-delivery/phase-0-seam-20260920c/reviews/shadow-rust.md' }. shadow-rust journaled zero artifacts.
The preceding step makes the pattern unambiguous. implement-rust journaled 6,837 artifacts:
top-level prefixes: {'target': 6832, 'crates': 5}
target/ and crates/ only — and nothing under .workflow-artifacts/, despite that step provably having written evidence/mutation-proof.md there:
$ stat -f "%Sm %N" -t "%H:%M" .workflow-artifacts/.../evidence/mutation-proof.md
08:48 .workflow-artifacts/.../evidence/mutation-proof.md
So the exclusion is not .gitignore-driven: target/ is gitignored too and is included in full. The distinguishing property of the missing path is the leading dot.
Why it matters
artifact_exists is documented as the journal-honest way to check "the agent wrote X", and it is the gate an author naturally reaches for on a review or report step. Silently excluding a whole class of paths means:
- the gate fails on a file that exists, with no indication why;
- the failure is a bare
retries_exhausted exit=1 (the lowered gate decides in-process, so there is no command output to inspect either);
- agent-written evidence is invisible in the journal generally, not just to this gate — any consumer reading
output.artifacts to find what a step produced will miss it.
What to change
Either include dot-directories in the worker's artifact scan, or — if they are excluded deliberately, which is defensible for .git/, .relayflowd/ and friends — make the exclusion explicit and inspectable:
- document which prefixes are excluded;
- have
flows check refuse an artifact_exists gate whose path falls inside an excluded prefix, with a message naming the exclusion, rather than compiling a gate that cannot pass;
- consider an opt-in (
artifacts.include) so a flow can declare its own artifact directory.
A deny-list of exactly .git/ and the runtime's own state directories would cover the real hazard without swallowing every author-chosen artifact path.
Acceptance
- Either an agent writing
.workflow-artifacts/x/y.md has that path in its journaled artifacts, or flows check refuses the artifact_exists gate naming it, before the run starts.
- A test pins a dot-directory artifact path either way.
Out of scope
Summary
An
artifact_existsgate can never pass for a file under a dot-directory, because the worker's journaledartifactslist omits those paths entirely. The gate is correct by its own rules — it reads the journal — but the journal does not describe what the agent actually wrote..workflow-artifacts/is the conventional artifact directory inAgentWorkforce/relay(every flow spec in that repo writes there), so in practiceartifact_existsis unusable for the thing it exists to check.Evidence
Run
01M2ZQP6P9ME3PRBWDXQA32MT3, CLI 2.0.22, macOS arm64,flows run --local-agent.The run died at
shadow-rust.gatewith the file it was gating sitting on disk:The gate was
{ type: 'artifact_exists', path: '.workflow-artifacts/migrate-native-delivery/phase-0-seam-20260920c/reviews/shadow-rust.md' }.shadow-rustjournaled zero artifacts.The preceding step makes the pattern unambiguous.
implement-rustjournaled 6,837 artifacts:target/andcrates/only — and nothing under.workflow-artifacts/, despite that step provably having writtenevidence/mutation-proof.mdthere:So the exclusion is not
.gitignore-driven:target/is gitignored too and is included in full. The distinguishing property of the missing path is the leading dot.Why it matters
artifact_existsis documented as the journal-honest way to check "the agent wrote X", and it is the gate an author naturally reaches for on a review or report step. Silently excluding a whole class of paths means:retries_exhausted exit=1(the lowered gate decides in-process, so there is no command output to inspect either);output.artifactsto find what a step produced will miss it.What to change
Either include dot-directories in the worker's artifact scan, or — if they are excluded deliberately, which is defensible for
.git/,.relayflowd/and friends — make the exclusion explicit and inspectable:flows checkrefuse anartifact_existsgate whosepathfalls inside an excluded prefix, with a message naming the exclusion, rather than compiling a gate that cannot pass;artifacts.include) so a flow can declare its own artifact directory.A deny-list of exactly
.git/and the runtime's own state directories would cover the real hazard without swallowing every author-chosen artifact path.Acceptance
.workflow-artifacts/x/y.mdhas that path in its journaledartifacts, orflows checkrefuses theartifact_existsgate naming it, before the run starts.Out of scope
artifact_existsshould consult the disk (it should not; journal-honest replay is the right design).subprocess_gatestdio loss (A failing subprocess_gate journals empty stdout/stderr: the lowering uses stdio:'inherit' and the daemon's stdio is captured nowhere #511), though the two combine badly: neither gate mechanism can tell an author why it failed.