v0.15.0 — The Workspace Policy Layer
PR: #1122 Date: 2026-08-23
The goal as understood at the end of the previous PR
PR #1110 (2026-08-23) closed out Phase 1 of the road-to-autonomous-ops program: role-scoped GitHub App identities, fail-closed role resolution (#423/#569), live-validated end-to-end. That left synlynk's own repo able to identify which role was acting, but with no machine-readable answer to "is this role authorized to do this" — that answer still lived as prose tables in CLAUDE.md (Capability-Based Task Allocation, PR Review Discipline, Named Release Policy), hand-read by whichever agent happened to be doing the work. Full autonomous operation for synlynk's own repo, targeted for 2026-08-31, needed those tables to become data a program can actually gate on — and needed that data to be per-workspace configurable from the start, since Phase 2 (rxcc) and Phase 3 (cc-videoreframing) are already known to need materially different policies (rxcc in particular, for DPDP/ABDM regulatory reasons).
Strategic shift in this PR
The brainstorm session that produced the design spec
(docs/superpowers/specs/2026-08-23-workspace-policy-and-autonomous-loop-design.md)
scoped this as two Named Releases rather than one big-bang PR: v0.15.0
"Workspace Policy Layer" (the authority mechanism itself, landing on
synlynk's own repo first) and v0.16.0 "Autonomous Loop" (the
awaiting_approval event, the approval-gate flow, and synlynk tpm sweep —
the loop that actually uses the authority mechanism unattended). This post
covers v0.15.0 only; v0.16.0 is next.
What this PR shipped
Two-tier policy schema (synlynk/policy.py): workspace defaults at
~/.synlynk/workspaces/<name>/policy.json (falling back to an in-code
DEFAULT_WORKSPACE_POLICY when absent — new workspaces get sane behavior
with zero config), merged with a repo's sparse .synlynk/policy.json
overrides. The merge rule is deliberately simple: whole-object replace per
top-level key under defaults, not a recursive deep merge. A repo that wants
to change merge_authority writes the whole merge_authority object; it
doesn't patch individual fields inside it. That keeps a repo-override file
short and its meaning unambiguous by inspection — no need to trace two files
to know what a merged key actually resolves to.
check_authority(action, role, repo_path) → AuthorityResult: covers
roadmap_edit, goal_create, merge, release_cut, and
task_dispatch:<type> actions, plus an approval_required_for rule matcher
(currently: named releases, roadmap-authority changes, security-sensitive
paths, irreversible merges — the approval ticket mechanism itself is
v0.16.0's job, this release only produces the requires_approval signal).
Four fail-closed call sites, matching #569's established style
(RuntimeError with an explicit reason, not a silent no-op):
dispatch_agent()'s task-allocation resolution, cmd_release,
cmd_roadmap_add, cmd_goal_create.
Three new CLI commands: synlynk policy check-merge (reviewer-facing —
non-zero exit means don't merge), synlynk policy sync-branch-protection
(idempotent; PUTs GitHub's branch-protection API using required checks
derived from .github/workflows/), synlynk policy show (prints the fully
resolved workspace+repo policy — the new source of truth for "who can do
what here," replacing hand-read prose).
This repo's own migration: .synlynk/policy.json now encodes exactly
what CLAUDE.md's tables said in prose — qa merges after non-authoring
review, pm cuts releases with human approval, the dev-authority
task-allocation table (Codex for implement/test/refactor/cli-plumbing, Agy
for css/templates/content/subpages, Grok for canvas/js/infra, Claude for
gh_write). CLAUDE.md's Capability-Based Task Allocation section is now a
five-line pointer to synlynk policy show instead of a hand-maintained
table — one fewer place to forget to update.
A regression caught mid-review, not by CI first: the default
role="dev" left on cmd_release/cmd_roadmap_add/cmd_goal_create
didn't match the default policy's pm-only authority for those actions, so
any call that didn't pass --role explicitly — including coldstart's own
first roadmap-row write — started failing closed. CI on PR #1122 caught it
first, actually (all three Python versions red); fixed in 20cb59c by
defaulting those three commands to role="pm", matching what they're
actually gating on. Worth calling out because it's exactly the failure mode
fail-closed design is supposed to produce loudly instead of silently — it
did.
Live verification, not just "tests pass": synlynk policy sync-branch-protection was run against the real nikhilsoman/synlynk repo
after PR #1122 merged to main, then independently confirmed via
gh api repos/nikhilsoman/synlynk/branches/main/protection —
required_status_checks = ["test (3.8)", "test (3.10)", "test (3.12)", "qa-gate"], required_reviews = 1, enforce_admins = true. This is
Phase 1's original stated exit criterion ("branch protection can be turned
on for real") and it's now independently checkable, not just asserted.
Brainstorm visuals
None — this design session stayed text-only; the schema and merge rule were simple enough not to need a visual companion.
What this achieved on the path to full autonomy
The mechanism that decides "is this action allowed" now exists as data,
gates real call sites, fails closed the way #569 established, and is proven
live on the one repo currently exercising it. Nothing about it hardcodes
synlynk's specific choices as universal — a repo with no .synlynk/policy.json
at all still gets sane DEFAULT_WORKSPACE_POLICY behavior, and a repo that
wants rxcc's more conservative posture writes its own override file with no
code change required. That's the portability property Phase 2/3 rollout
needs, built in from the first repo rather than retrofitted later.
The goalpost at the end of this PR
v0.16.0 "Autonomous Loop" is next: the awaiting_approval GOVERNS event
type, the approval-gate flow (gh issue create + resolution detection
extending scan_local_events()), and synlynk tpm sweep — the command that
actually walks ready stories through dispatch→verify→PR→review→merge
end-to-end, gated on the authority mechanism this release built, parking on
requires_approval without blocking the batch. The 2026-08-31 exit
criterion is a live dogfood run of that loop, including at least one story
that deliberately trips an approval rule, proving the pause+ticket+resume
path works unattended — not just the happy path.