PR #23 / #24 — v0.2.2: Attribution in a Polyglot World

PR #23 / #24 — v0.2.2: Attribution in a Polyglot World

PRs: fix: resolve GitHub username and upgrade check via gh CLI + chore: bump version to 0.2.2
Merged: 2026-05-20
Tests: 47 (no new tests — existing tests updated to mock gh CLI correctly)


The Broader Goal at This Point

Still: reliable solo workflow. The kernel was running. Correctness was patched. The next concern was robustness in real environments — specifically, environments where the assumptions built into v0.2.1 turned out to be wrong.


What This PR Fixed

The username resolution chain

get_username() resolves the identity of the current user for devlog attribution. In v0.2.1, the implementation called git config user.name and used the result.

git config user.name is unreliable as a primary identity source for several reasons:

  • In CI or automated environments, it may be set to a generic value ("actions-user", "root") that does not correspond to any real contributor.
  • In a multi-agent workgroup where agents run as different git identities (e.g., feat/gemini/ branch prefixes), the git config reflects the current actor, not the intended project contributor.
  • In some shell environments (fresh login, container launches), git config may not be configured at all.

The fix introduced a two-level fallback chain:

def get_username() -> str:
    # Level 1: GitHub authenticated identity (canonical, verified)
    try:
        result = subprocess.run(
            ["gh", "api", "user", "--jq", ".login"],
            capture_output=True, text=True
        )
        login = result.stdout.strip()
        if login and result.returncode == 0:
            return login
    except Exception:
        pass
    
    # Level 2: git config (local, unverified but sufficient for most cases)
    try:
        result = subprocess.run(
            ["git", "config", "user.name"],
            capture_output=True, text=True
        )
        name = result.stdout.strip()
        return name.lower().replace(" ", "") if name else "unknown"
    except Exception:
        return "unknown"

The gh api user call returns the authenticated GitHub login — a verified identity tied to a real GitHub account. This is the canonical identity for attribution in a GitHub-centric workflow. If gh is not installed or the user is not authenticated, the git config fallback maintains backward compatibility.

The upgrade check via gh CLI

synlynk upgrade checks GitHub releases for a newer version. The v0.2.1 implementation used urllib.request directly against the GitHub API. This worked but had two issues:

  1. It consumed the unauthenticated API rate limit (60 requests/hour per IP), which could fail in CI environments or on shared networks.
  2. It did not benefit from the user's existing GitHub authentication.

The fix added a gh api path:

def check_upgrade():
    # Try gh CLI first (authenticated, higher rate limits)
    try:
        result = subprocess.run(
            ["gh", "api", "repos/nikhilsoman/synlynk/releases/latest", "--jq", ".tag_name"],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            latest = result.stdout.strip().lstrip("v")
            # compare with VERSION...
    except Exception:
        pass
    # Fallback: urllib (unauthenticated)
    ...

Why Attribution Is Harder Than It Looks

These two bug fixes look like plumbing. They are actually the first encounter with a problem that becomes structural in the agent identity design:

In a multi-agent workgroup, "who did this?" is not a simple question.

Consider the attribution chain for a single commit in the RxCC workgroup:

  • git config user.name → "nikhil" (the machine's git config, set by the human)
  • git config user.email → nikhilsoman@gmail.com
  • Branch prefix → feat/gemini/ui-redesign (the agent that did the work)
  • Co-Authored-By: trailer → "Claude Sonnet 4.6 noreply@anthropic.com" (the reviewer)
  • GitHub issue agent:gemini label → the routed agent
  • costs.md row → user: gemini, cost: $0.24

These are five different attribution signals, each correct in a different sense. The git commit author is the human (whose machine credentials were used). The branch prefix is the agent (whose work this actually is). The Co-Authored-By trailer is the reviewer. The label is the router's attribution. The costs row is the financial attribution.

get_username() for devlog purposes wants the session actor — who was running this session, whether human or agent. The GitHub login is the closest approximation, because it is verified and stable. But it still conflates the human account holder with the agent running under that account.

This tension — one GitHub account, multiple agents — is what the v0.5.0 agent identity design resolves with the two-layer model: Local Identity (Ed25519 keypair, agent_uuid) as the cryptographic anchor, and GitHub login as the audit label. The keypair is per-agent; the GitHub login is per-account-owner. They are different things and should not be conflated.

v0.2.2 was the first place this distinction appeared as a practical problem rather than a theoretical concern.


The Test Failures That Exposed the Design

Three pre-existing tests were failing after v0.2.0:

  • test_get_username_from_git
  • test_upgrade_reports_new_version
  • test_upgrade_handles_network_error

These tests mocked the wrong subprocess call. They expected subprocess.run(["git", "config", "user.name"]) to be the only identity resolution path. After the gh CLI fallback was added, the mock did not account for the gh call being attempted first — and the mock's subprocess.run intercept caught the gh call as an unexpected invocation.

The fix was to mock both paths correctly, in the right order. But the more important lesson: the test failures were correct signals. The tests were catching a design assumption that was now wrong. The gh CLI path is the primary path; the git config path is the fallback. A test that only mocks the fallback is not testing the primary behavior.

This is a recurring pattern in the synlynk test suite: tests that pass when behavior is wrong (because they mock at the wrong level) and tests that fail when behavior is right (because the mock doesn't match the new design). The right response to a "pre-existing failure" is to understand what assumption changed, not to mark it as known-flaky.


What This Achieved on the Path to Autonomy

Attribution is the accounting primitive for autonomous operation. Every autonomous action must be attributable:

  • Which agent performed this story?
  • Under whose authority was it dispatched?
  • Who signed off on the result?
  • What did it cost?

v0.2.2's username resolution chain is the v0.2.x approximation of what becomes the full identity stack in v0.5.0: Ed25519 keypair → agent_uuid → GitHub login as audit label. The fallback chain (verified identity → local config → "unknown") anticipates the entitlement hierarchy (authorization token → role claim → public key).

The gh CLI as the verified identity source also anticipates the synlynk start command in v0.4.0, which calls gh api directly to move board items and set the Agent field — the harness acts on behalf of the authenticated GitHub user, making GitHub the identity substrate for all board operations.


Strategic Note

No goalpost movement. Robustness and correctness remained the theme. But the username resolution problem planted a flag: multi-agent attribution is a first-class concern, not an afterthought. The observation that "one GitHub account, multiple agents" is an underspecified identity model was noted and tabled.

It would resurface in the v0.5.0 design as the core motivation for the Ed25519 machine-level keypair.


Next: PR #26 — v0.3.0: The Multi-Agent Foundation — the hybrid workgroup studies land, the goal shifts from context injector to workgroup orchestrator, and the first deliberate multi-agent primitives ship.