PR #39 — v0.4.0: The Hybrid Workgroup Bootstrap

The Broader Goal at the End of the Previous PR

After PR #30 landed the E2E safety net, the next milestone was clear: ship v0.4.0 — the release that turns synlynk from a single-agent context injector into something that knows it's coordinating a team. The four imperatives identified in the brainstorm (post #10) set the agenda: intelligent init, agent discovery, shell-native dispatch, and the workgroup identity contract. All four had to land in this PR for the Magic Moments to be real.

Strategic Shifts in This PR

Two things sharpened during implementation that weren't fully resolved in the design:

1. The Tokq memory unit schema gap got fixed first. Before writing a line of implementation, we discovered that the v0.4.0 design spec mapped synlynk files to Tokq memory units at the wrong grain — file-level instead of semantic-purpose-level. We redesigned the mapping into five purpose-typed DB view units (strategic, context, execution, activity, capability) and committed a visual explainer in docs/brainstorm/tokq-data-metamorphosis/. This didn't change what v0.4.0 ships, but it meant the foundation we're building on won't need a breaking schema migration at v1.0.

2. PermissionError is not the same as "process is dead." When wiring PID reconciliation, the first draft caught (ProcessLookupError, PermissionError) in _reconcile_jobs(). Code review caught the semantic error: PermissionError from os.kill(pid, 0) means the process exists but we can't signal it — marking a live process as failed would be wrong. The fix was except ProcessLookupError: only, and the test was updated to monkeypatch _reconcile_jobs rather than rely on PID 1 (which raises PermissionError on macOS).

What This PR Shipped

Dispatch model from the design session
v0.4.1 architecture diagram

Fourteen TDD tasks, 11 commits, 43 net-new tests (183 total, all green):

Constants and Baselines (Task 1)

VERSION = "0.4.0", JOBS_FILE, LOGS_DIR, PROMPTS_DIR, AGENT_CAPABILITY_BASELINES (claude/gemini/codex/agy with cli, roles, strengths, non_interactive_flags), AGENT_DISCOVERY_DEFAULTS, ANSI color helpers.

Job Store (Task 2)

_load_jobs(), _save_jobs(jobs), _reconcile_jobs(). The reconciler iterates running jobs, probes each PID with os.kill(pid, 0) (signal 0: existence check, no actual signal), marks unreachable ones failed with an ISO timestamp, and only writes back if something changed. Called as the first line of main().

Agent Discovery (Task 3)

_check_agent_functional(cli) — runs <cli> --version with a 5-second timeout, returns the first line of stdout on success, None on FileNotFoundError/TimeoutExpired/OSError. discover_agents(config) — merges AGENT_DISCOVERY_DEFAULTS with per-project overrides from .synlynk/config.json["agent_discovery_paths"], skips agents whose config directory doesn't exist, returns a list of agent dicts with functional flag.

Two-Pass Semantic Init (Tasks 4–6)

_static_scan(root) reads up to 50 git log entries (--oneline --no-merges), extracts the README H1 as project name, sniffs languages from file extensions, counts total commits via git rev-list --count HEAD, and detects conventional commit style (>= half of recent messages match feat:/fix:/chore:/docs:/test:/refactor:/perf:). _write_informed_skeleton(scan, skip_existing) generates project-docs/roadmap.md, memory.md, todo.md with project name, description, language list, and recent commit topics injected — plus a caveat banner for repos without structured commits. _llm_enrich(agent_cli, scan) writes a prompt to .synlynk/prompts/llm-enrich.md and calls the agent non-interactively via subprocess.run(cmd, stdin=pf, timeout=120), replacing roadmap.md with the enriched output on success. Best-effort: broad except Exception: return False.

Init Wizard (Task 7)

Replaced the init() body with a 6-step progressive TUI:

  1. _static_scan(".") — prints project name, commits, language, first recent topic; warns if commits aren't structured
  2. discover_agents()Magic Moment 1: "✨ Your Hybrid Workgroup is ready:" table of functional agents with roles and versions
  3. Create dirs → _write_informed_skeleton() → write agent instruction files via _build_templates()
  4. LLM enrichment offer — only if functional agents found; input("[y/N]")
  5. Cloud/team nudge — input("Email or synlynk ID:") stored to config as workgroup_invite_email
  6. Write .synlynk_config.json, _update_config(), set_state("stopped"), print Magic Moment 2 dispatch prompt

Both input() calls are wrapped to swallow EOFError for non-interactive invocations.

Background Dispatch (Task 8)

dispatch_agent(agent, task, story_id) — validates agent against AGENT_CAPABILITY_BASELINES (raises ValueError for unknowns), generates a unique job ID via MD5(agent+task+time.time())[:8], calls generate_context(scope="full") (swallowing exceptions), writes prompt to .synlynk/prompts/<job_id>.md, and launches via:

subprocess.Popen(cmd, stdin=prompt_in, stdout=log_out,
                 stderr=subprocess.STDOUT, start_new_session=True)

start_new_session=True detaches from the terminal — the job survives terminal close. Returns the job dict and persists to jobs.json.

Shell Commands (Tasks 9–13)

  • cmd_jobs(all_jobs) — reconciles, then prints a formatted table of running (or all) jobs with ANSI status colouring
  • cmd_logs(job_id, tail) — prints last N lines of .synlynk/logs/<job_id>.log
  • cmd_shell(story_id) — spawns $SHELL with SYNLYNK_PROJECT_DIR, SYNLYNK_STORY_ID, SYNLYNK_CONTEXT injected
  • cmd_launch(agent, story_id) — copies context.md to context-<agent>.md, runs agent interactively, logs telemetry on exit
  • cmd_run_trio(task, story_id) — dispatches all functional agents in parallel (Magic Moment 2); warns if fewer than 3 available

Subcommand Wiring + E2E (Task 14)

Six new argparse subcommands registered in main(): dispatch, jobs, logs, shell, launch, run (with --trio sub-action). _reconcile_jobs() added as the first call inside main(). Four new E2E tests in test_e2e.py validate dispatch end-to-end (fake claude binary via PATH), jobs empty state, logs missing-job error, and startup reconciliation of a stale PID.

Brainstorm Visuals Used

  • docs/brainstorm/tokq-data-metamorphosis/tokq-memory-metamorphosis.html — informed the pre-implementation gap fix on memory unit grain; not directly relevant to v0.4.0 features but locked in the schema we're building toward

What This Achieved on the Path to Autonomy

Before this PR, synlynk init wrote static template files. After this PR:

  • synlynk sees your repo (git history, README, file tree, languages)
  • synlynk discovers which agents are installed and functional
  • synlynk shows you your team — the Hybrid Workgroup — the moment you init
  • You can synlynk dispatch claude --task "implement auth fix" from a shell and the job runs in the background with its output captured
  • synlynk jobs gives you a live-reconciled view; synlynk logs --job <id> tails the output
  • synlynk run --trio --task "..." dispatches all three agents in parallel with one command

This is the first version where the phrase "multi-agent dispatch" is literally true at the shell level.

Strategic Note: The Goal at the End of This PR

v0.4.0 delivers the dispatch plane. The next release (v0.5.0, Capability Engine) needs to make the routing smart: instead of sending every agent the same task, route by capability — architect tasks to claude, verification to gemini, fast edits to codex. That requires capability.json → SQLite to land, and AGENT_CAPABILITY_BASELINES to become a data-driven registry rather than a hardcoded dict. The constants and structures laid down in this PR are the stable foundation for that work.