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/protectionrequired_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.