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
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:
_static_scan(".")— prints project name, commits, language, first recent topic; warns if commits aren't structureddiscover_agents()— Magic Moment 1: "✨ Your Hybrid Workgroup is ready:" table of functional agents with roles and versions- Create dirs →
_write_informed_skeleton()→ write agent instruction files via_build_templates() - LLM enrichment offer — only if functional agents found;
input("[y/N]") - Cloud/team nudge —
input("Email or synlynk ID:")stored to config asworkgroup_invite_email - 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 colouringcmd_logs(job_id, tail)— prints last N lines of.synlynk/logs/<job_id>.logcmd_shell(story_id)— spawns$SHELLwithSYNLYNK_PROJECT_DIR,SYNLYNK_STORY_ID,SYNLYNK_CONTEXTinjectedcmd_launch(agent, story_id)— copiescontext.mdtocontext-<agent>.md, runs agent interactively, logs telemetry on exitcmd_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 jobsgives you a live-reconciled view;synlynk logs --job <id>tails the outputsynlynk 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.