0059 — A propflowai push refreshes Smith's runtime clone: one webhook, a second repo key, a path filter
- Status: Accepted 2026-10-15 (model-delegated; open to override on block
b92097120) — the SHAPE is decided and recorded here at decision time, before any mechanism exists. No PR is cited below because none had been opened when this was written, and nothing has merged. Written per ADR-0004. (The renderer's badge takes only this first line, which is why the caveat is on it: a reader of the status alone would otherwise take this for a human ratification it is not.) - Date: 2026-10-15
- Deciders — split, because two different kinds of authority are at work and
attributing one to the other is the defect this file exists to prevent:
- The shape is the Driver's (session
ff9239cf, docksmith-alerts-noise-diet), ruled 2026-10-15 after the decision ladder ruled that the shape was a model's to decide — receiptf574da770(Fable,claude-fable-5), concurring receiptff7b390f3(Astra), Fable and Astra independently choosing the same option. A model-resolved question cannot be re-raised, so it is applied here. - ⚠️ THE DRIVER'S OWN RECOMMENDATION WAS THE OPPOSITE, and that is recorded
rather than quietly dropped. It raised block
b92097120recommending "hold the shape for the listener's owner", because the listener is a running service with no automatic check behind it. The ladder overruled that recommendation. Gera has not seen the question; the block carries the ruling, and that channel stays open — this ADR records a model's delegation and a Driver's shape, not Gera's ratification of either. - An earlier run of the same question returned NEEDS HUMAN on a
POSITION-BIAS FLIP (receipt
f197e754b): with the options as given it chose "hold for the owner" at high confidence, and with the same two options reversed it chose "the session decides", also high. That run is cited because an answer a reordering can flip is not evidence, and a later concurring run does not retroactively make the earlier one sound. What makes the applied ruling usable is the independent concurrence, not the count of runs. - The deploy of the resulting change is NOT covered by this ADR. See The boundary below.
- The shape is the Driver's (session
The problem, measured
Smith reads propflowai's contract files at runtime from a clone called
propflowai-smith. config.propflowai_root() resolves in four steps —
SMITH_PROPFLOWAI_ROOT (explicit override), then SMITH_HOME.parent.parent
(the pre-split in-tree layout), then $PROPFLOW_ROOT/propflowai-smith (the
runtime clone, "the checkout the Construct trusts"), then PROPFLOWAI_DIR (a
teammate's primary checkout); candidates 2-4 qualify only if they actually hold
config/automations.toml, and none qualifying raises rather than defaulting
silently. So on the mini the clone is what Smith reads unless an in-tree copy
outranks it — stated in full because an implementer reading "prefers the clone"
would miss that candidate 2 can win. And
~/.claude/scripts/smith-sync-from-main.sh:55-56 sets
PF_REPO=$HOME/code/PropFlow/propflowai-smith with:
CONFIG_FILES=(config/automations.toml config/channels.toml
scripts/print-sensor-registry.ts
src/lib/platform/observability/sensor-registry.ts)
The only automatic caller of that script is the push-deploy in the review
webhook, and it is keyed on ONE repo
(~/.claude/services/review-webhook/listener.js):
:1485 const SMITH_DEPLOY_REPO = process.env.SMITH_DEPLOY_REPO || 'PropFlow-Technologies/agent-smith'
:1486 const SMITH_DEPLOY_REF = process.env.SMITH_DEPLOY_REF || 'refs/heads/main'
:1556 if (repo !== SMITH_DEPLOY_REPO || ref !== SMITH_DEPLOY_REF) { … }
A push to propflowai fires nothing. Both constants are env-overridable, so today's keying is a default rather than a hardcode.
⚠️ This is not a race, it is the default ordering. The ordinary deploy order merges agent-smith first and the propflowai manifest PR second, because the code usually has to land before the manifest describes it. So the sync runs before the manifest lands, and the clone holds the OLD manifest until the next agent-smith push — which may be days away and will be about something unrelated.
Why it matters beyond a stale file: the drift guard that compares Smith's
daemon roster against the manifest reads that clone, so through the whole
window it compares against the old manifest and either misses a real mismatch or
reports a false one. Nothing automatic catches that: the guard skips in CI for
want of a propflowai checkout, it is not in ci/cross-repo-tests.txt, and the
local pacer refuses it as unchanged. Two independent gaps compose — nothing
refreshes the input, and nothing runs the check.
The decision
One webhook, a second repo key, a path filter. The existing push-deploy in the review webhook gains a second repo+ref it will accept, and for that second repo it refreshes the clone only when the push touches a file in the synced set. The agent-smith trigger and the deploy's existing batch-guard behaviour are untouched.
Specifically:
- Extend the keying, do not add a mechanism. No second poller, no new service, no cron. ADR-0002 already fixes the shape of coupling between fleet parts, and a poller for a signal that is already arriving as a webhook is a second copy of a thing that works (Constitution §1). The precedent for extending this listener's event handling is ADR-0039.
- The second repo's trigger is path-filtered; the first repo's is not. An agent-smith push deploys code and must always deploy. A propflowai push is relevant only when it changes a file Smith reads, and propflowai pushes far more often than Smith does — an unfiltered second key would run the sync on every unrelated propflowai merge.
- The filter is derived from the synced set, not restated.
CONFIG_FILESinsmith-sync-from-main.shis the existing source of truth for what the clone carries. A second hand-written copy in the listener is drift with a start date (Constitution §1), and the failure mode is silent: a file added toCONFIG_FILESlater would sync on an agent-smith push and never on a propflowai one. ⚠️ HOW it derives is left to the implementer, but the failure mode is not. The listener is JavaScript and the source of truth is a bash array, so the options are: parse the script, have the script print the list behind a flag (--print-config-files), or move the list to a shared data file both read. Each has a different failure mode, and the dangerous one is shared: a derivation that quietly returns an EMPTY or unparseable set means no propflowai push ever matches — which is exactly today's defect wearing a new mechanism, and invisible for the same reason. So the pin has a second condition: red on an empty or unparseable derived set, not only onconfig/automations.tomlfalling out of it. - State which way a truncated payload fails, and it should fail OPEN. A
GitHub push payload carries
added/modified/removedper commit, and that detail is truncated on large pushes and absent in some shapes. A filter that reads those lists and finds nothing relevant cannot tell "no relevant path" from "the list was cut", and failing closed there is silent in the same way as the empty-set case. Refreshing is cheap and idempotent, so an indeterminate path list refreshes. - Additive only (Constitution §6): the existing trigger keeps working unchanged, and if the second key is wrong the only loss is the refresh that does not happen today either.
⚠️ A PREREQUISITE THAT IS NOT OPTIONAL, AND IT IS GERA'S
propflowai's webhook to the listener does not deliver push today. Measured
2026-10-15, both repos' hooks for the listener's own endpoint
https://gh-webhook.propflowai.co/gh:
propflowai hook 625249155 push=false
events: check_suite, deployment_status, issue_comment, pull_request,
pull_request_review, pull_request_review_comment
agent-smith hook 653865817 push=true
events: issue_comment, pull_request, pull_request_review,
pull_request_review_comment, push
(Each repo also has a webhook-forwarder.github.com hook that does carry
push; that is a different destination and does not reach this listener.)
So without push added to propflowai's hook 625249155, every line of this
decision is dead on arrival — the listener would be correct and would never be
called. ADR-0039 hit the same wall and had to add deployment_status to that
repo for the same reason. It is a repo setting, so it is Gera's, and it belongs
with the boundary below rather than in the implementing PR. This was raised by
the review bot on the PR that landed this ADR, not by the author — the ADR had
specified a mechanism whose delivery precondition was absent.
It ships its pin, and the pin is about the trigger's reach
A test that goes RED if the trigger's repo/paths set stops covering
config/automations.toml. This is the condition, not a suggestion: a
behaviour change here with no pin is how a silent trigger gets dropped in one
line later, and the failure is invisible — nothing refreshes, nothing errors,
and the drift guard keeps comparing against a stale file exactly as it does
today.
⚠️ The pin must fail when the behaviour is removed, not when a string moves.
On the dock that produced this ADR, five guards were found in one day that
passed on every real case and could fail only on impossible ones — four of them
had positive controls and all four passed. A test asserting that a filename
appears in the listener's source is a text match wearing a behaviour test's
clothes. Mutate the artifact: remove the second repo key, assert red; remove
config/automations.toml from the covered set, assert red.
The boundary — what this ADR does NOT authorise
It does not authorise editing or restarting the running listener. The shape
is decided; the deploy is a live-service change, and this fleet's standing rule
holds infra blast radius — triggers, crons, alarms — for Gera, who owns that
service. So the implementing work is: a reviewed change plus its pin, landed as
a PR, and the restart named as his step, with the risk stated in a sentence
when it is handed over. SYSTEMS.md's review-webhook entry documents the edit
procedure (.bak file, node --check, kickstart) and that procedure is part
of the hand-over, not a licence to run it unasked.
If that reading is wrong — if Gera would rather the implementing session also
deploy it — he says so on block b92097120, which is still open and carries
this ruling.
Consequences
- A propflowai manifest change reaches Smith's runtime on the next relevant delivery instead of waiting for an unrelated agent-smith push. Not "within one delivery": a sync can race a merge-queue or a follow-up push, and the correction is that the next relevant push re-syncs. The window closes from days to the next push, which is the claim worth making.
- The drift guard's input stops being stale by default. The guard still does
not run automatically — that is a separate gap, recorded on dock row
a-deleted-workflow-is-still-cited-as-where-these-tests-runand documented by agent-smith PR #1123. Fixing the input does not fix the check, and this ADR should not be read as closing both. - The listener grows a path filter, which is new logic in a service whose failures are quiet. That is the cost, and it is why the pin is a condition rather than a nicety.
SYSTEMS.md'sreview-webhookentry needs itssignalsupdated when the mechanism lands, so a reader can tell whether the second trigger is live.
What was rejected
- A poller watching propflowai's HEAD. Rejected on ADR-0002: a second mechanism for a signal already arriving, and it would add a liveness surface nobody watches.
- Syncing on a timer. Rejected because it makes the staleness window a constant rather than closing it, and because a timer that fires while a propflowai merge is mid-flight reintroduces the ordering problem it was meant to solve.
- Removing the runtime clone and reading
PROPFLOWAI_DIRdirectly. Rejected as out of scope here and larger than the defect: the clone exists so Smith keeps working on a machine without a full propflowai checkout, andload_manifest()raising there is load-bearing forsmith doctor.