v0.16.0 — The Autonomous Loop
PRs: #1125, #1126, #1127 Date: 2026-08-23
The goal as understood at the end of the previous PR
PR #1122 (2026-08-23) shipped v0.15.0, the Workspace Policy Layer: a two-tier
policy.json (workspace defaults + per-repo overrides) and a fail-closed
check_authority() resolver that turned this repo's own prose tables
(who can merge, who can cut a release, which harness handles which task
type) into data, gated at four call sites, with branch protection live-
verified on main. That release deliberately stopped at the signal —
check_authority() could tell a caller "this needs human approval" but
nothing consumed that signal unattended. v0.16.0 was scoped, from the same
brainstorm session, as the release that actually closes the loop: the
awaiting_approval event, the ticket-based pause mechanism, and the sweep
command that walks stories through dispatch without a human driving each
step — the 2026-08-31 full-autonomy exit criterion for synlynk's own repo.
Strategic shift in this PR
No scope change from what v0.15.0's own post already previewed — this is
the second of the two Named Releases the original brainstorm anticipated,
landing as planned. The one real-time adjustment happened inside Task 13's
execution, not before it: the live dogfood run was supposed to exercise
synlynk tpm sweep against "a story that trips an approval rule," but the
plan's assumption that some default policy rule would naturally gate
task_dispatch: actions turned out to be false — _matches_approval_rule()
explicitly skips security_sensitive_paths: rules for dispatch actions,
and no other default rule covers that action type at all. Rather than
silently declaring the pause path untestable, a temporary, clearly-labeled,
fully-reverted test-only rule (task_dispatch_demo) was added to this
repo's .synlynk/policy.json for the duration of the demo, removed
immediately after, and confirmed removed via an empty
git diff origin/main -- synlynk/policy.py .synlynk/policy.json. The demo
also surfaced that the live backlog had 8 real ready stories that an
unscoped sweep would have dispatched for real — caught before running the
sweep, and handled by temporarily parking those 8 (synlynk story draft)
and restoring them afterward, rather than letting the dogfood run become an
unintended production dispatch of unrelated work.
What this PR shipped
awaiting_approval GOVERNS event (Task 10, PR #1125) — extends the
job_terminal/review_submitted event-contract pattern from PR #922.
emit_awaiting_approval(story_id, action, reason) in synlynk/events.py
records which approval_required_for rule matched, giving every pause a
durable, queryable record instead of a transient console message.
Approval-gate ticket flow (Task 11, PR #1126) — synlynk/approval_gate.py's
raise_approval_ticket() files a [APPROVAL] <action> — <story_id> GitHub
issue carrying story context and the matched rule, with instructions to
reply approve or act directly. Resolution detection extends
scan_local_events() with a new _scan_approval_tickets() step: it polls
open [APPROVAL] issues and emits approval_resolved for any closed or
carrying an approve-prefixed comment.
synlynk tpm sweep (Task 12, PR #1127) — the command that actually uses
all of this: one pass over readiness='ready' stories, each walked through
dispatch → verify → PR → review → merge, with every step gated on
check_authority(). A requires_approval result parks that one story
(raises the ticket, emits the event) without blocking the rest of the
batch — the core design property that makes unattended operation safe
rather than all-or-nothing.
Live dogfood verification (Task 13, Claude-direct per the plan) — not a
unit test, a real run against this repo's real backlog. Pass 1 (no
approval rule active) dispatched story-028d26d9 for real, through to a
merged PR — proving the happy path end-to-end, not mocked. Pass 2 (the
temporary test-only rule described above) parked story-7aa0aaec, filed a
real [APPROVAL] GitHub issue, received an approve comment, and
correctly emitted approval_resolved (event id 272) — independently
confirmed via synlynk events tail --type approval_resolved and
gh issue view --json state,comments, not just the sweep's own printed
summary. Every other claim in the run — dispatch status, branch protection
state, ticket creation — was cross-checked the same way, per the plan's
explicit instruction not to trust self-reported status alone.
Two real gaps found by actually running it, not by reading the code —
scan_local_events() crashes on a subscriptions table schema bug before
it ever reaches _scan_approval_tickets(), and its only call site has no
wired CLI subcommand, so the documented resolution-detection path doesn't
run in production today (filed as
#1132). Separately,
synlynk story done doesn't clear readiness, and the sweep's in-flight
guard only excludes queued/running jobs — so a story whose dispatch
already completed gets re-swept and re-parked on the next pass, observed
live with story-028d26d9 (filed as
#1133). Per this
project's own role split, both were filed rather than patched inline —
fixing them is implementation work for a future dispatch, not Claude's
verification-only Task 13.
Brainstorm visuals
None — same as v0.15.0, this design session stayed text-only.
What this achieved on the path to full autonomy
The loop synlynk's own repo needed for the 2026-08-31 exit criterion now exists and has been proven live, not just designed: a story can be dispatched, verified, and merged with no human in the step-by-step path, and a policy-flagged action pauses cleanly with a ticket instead of either blocking the whole batch or silently proceeding unauthorized. The two bugs the dogfood run found are exactly the kind of gap that only shows up under a real run against a real backlog — a mocked or unit-tested version of this release would have shipped with the resolution-detection path silently broken.
The goalpost at the end of this PR
Two known gaps stand between this release and true unattended operation:
scan_local_events()'s approval-resolution path needs a working call site
(#1132), and re-sweep needs to stop re-parking already-completed stories
(#1133). Both are scoped narrowly enough to land as normal dispatched fixes
rather than a new Named Release. Beyond those two, the deferred
ticket-resolution-auto-resume gap (approving a ticket doesn't itself
unblock the next sweep pass) is the natural v0.17.0-scale increment once
the sweep loop has run unattended for real for a while — not blocking the
2026-08-31 exit criterion, but the next thing worth designing once this
release's rough edges are sanded down.