From 8a61fea59cea28d26f13727adb84b31943d045b0 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Aug 2026 12:43:21 -0400 Subject: [PATCH 1/4] feat: vendor stokowski for Linear-driven agent orchestration Vendors Sugar-Coffee/stokowski as a git submodule and checks in a shared workflow.yaml + .ai/prompts/*.md - safe to commit since tracker.api_key is omitted entirely, so Stokowski resolves it from the LINEAR_API_KEY env var at runtime rather than anything in the file. Adds `mix stokowski` (root repo_tasks project), which for now only validates workflow.yaml is safe (exists, gitignored if it ever does carry a literal key, no bare api_key literal) - actually launching a session is a follow-up. Removes the `lc` escript target: it never worked once exqlite (a NIF-backed dep) was added, since NIFs can't load from inside an escript archive, and nothing in CI or docs ever exercised it. Documents `mix run` as the supported way to invoke `lc` locally in app/usage-rules.md, along with a standing rule to dogfood `lc` itself for Linear access rather than an MCP server or other integration skill. --- .ai/prompts/global.md | 46 ++++++ .ai/prompts/implement.md | 73 +++++++++ .ai/prompts/investigate.md | 51 ++++++ .ai/prompts/merge.md | 52 ++++++ .ai/prompts/review.md | 64 ++++++++ .gitmodules | 3 + AGENTS.md | 1 + app/.gitignore | 4 - app/lib/linear_cli/application.ex | 33 ++-- app/lib/linear_cli/oban_repo.ex | 4 +- app/mix.exs | 5 - app/usage-rules.md | 17 ++ lib/mix/tasks/stokowski.ex | 77 +++++++++ mise.toml | 1 + test/mix/tasks/stokowski_test.exs | 11 ++ vendor/rubyists-homebrew-tap | 2 +- vendor/stokowski | 1 + workflow.yaml | 260 ++++++++++++++++++++++++++++++ 18 files changed, 676 insertions(+), 29 deletions(-) create mode 100644 .ai/prompts/global.md create mode 100644 .ai/prompts/implement.md create mode 100644 .ai/prompts/investigate.md create mode 100644 .ai/prompts/merge.md create mode 100644 .ai/prompts/review.md create mode 100644 lib/mix/tasks/stokowski.ex create mode 100644 test/mix/tasks/stokowski_test.exs create mode 160000 vendor/stokowski create mode 100644 workflow.yaml diff --git a/.ai/prompts/global.md b/.ai/prompts/global.md new file mode 100644 index 0000000..88d34d7 --- /dev/null +++ b/.ai/prompts/global.md @@ -0,0 +1,46 @@ +# Global Agent Instructions + +You are an autonomous coding agent running in a headless orchestration session. +There is no human in the loop — do not ask questions or wait for input. + +## Ground rules + +1. Read and follow the project's AGENTS.md for coding conventions and standards. +2. Never use interactive commands, slash commands, or plan mode. +3. Only stop early for a true blocker (missing required auth, permissions, or secrets). + If blocked, post the blocker details as a Linear comment and stop. +4. Your final message must report completed actions and any blockers — nothing else. + +## Execution approach + +- Spend extra effort on planning and verification. +- Read all relevant files before writing code. +- When planning: read AGENTS.md, the existing code in the area you are modifying, and any related docs. +- When verifying: run all quality commands (type-check, lint, tests), then review your own diff. +- If you have edited the same file more than 3 times for the same issue, stop and reconsider your approach. + +## Session startup + +Before starting any implementation work: + +1. Run the project's type-check command to verify the codebase compiles clean. +2. Run the project's test command to verify all tests pass. +3. If either fails, investigate and fix before starting new work. + +## Linear workpad + +Use a single Linear comment as a persistent workpad: + +- Title: `## Workpad` +- Update it at each milestone with: current status, decisions made, and next steps. +- On rework runs, append the rework section — do not delete prior content. + +## Rework awareness + +Every prompt in this workflow serves both first-run and rework cases. +On rework runs, the workspace already contains prior work. Check for: + +- An existing feature branch (do not create a new one) +- An open PR (push to it, do not open a second) +- Review comments requesting changes (address them specifically) +- Prior workpad content (append to it, do not overwrite) diff --git a/.ai/prompts/implement.md b/.ai/prompts/implement.md new file mode 100644 index 0000000..d94d3c3 --- /dev/null +++ b/.ai/prompts/implement.md @@ -0,0 +1,73 @@ +# Implementation Stage + +You are implementing the solution for **{{ issue.identifier }}**: {{ issue.title }} + +**Current status:** {{ issue.state }} +**Labels:** {{ issue.labels }} +**URL:** {{ issue.url }} + +## Issue description + +{% if issue.description %} +{{ issue.description }} +{% else %} +No description provided. +{% endif %} + +## Objective + +Implement the solution, create a PR, and ensure it passes all quality checks. + +## First run + +1. Read the investigation summary from the Linear comments. +2. Read the relevant source files identified in the investigation. +3. Create a feature branch from `main`: + ``` + git checkout -b {{ issue.identifier | lower }}- + ``` +4. Implement the changes with clean, logical commits. +5. Run the full quality suite: + - Type checking + - Linting + - All tests +6. Fix any failures before proceeding. +7. Push the branch and create a PR: + ``` + git push -u origin HEAD + gh pr create --title "{{ issue.identifier }}: " --body "" + ``` +8. Link the PR to the Linear issue. +9. Update the workpad with: what was done, what was tested, any known limitations. + +## Rework run + +If this is a rework run (a branch and PR already exist): + +1. Find the existing PR: + ``` + gh pr list --head + ``` +2. Read review comments and requested changes: + ``` + gh pr view --comments + ``` +3. Address each piece of feedback specifically. +4. Run the full quality suite again. +5. Push new commits to the existing branch (do not force-push). +6. Post a comment on the GitHub PR summarising the rework: + - Which review comments were addressed + - What was modified + - Any decisions or trade-offs +7. Append a rework section to the Linear workpad. + +## Quality bar + +Before finishing, verify: + +- [ ] All tests pass +- [ ] No type errors +- [ ] No lint errors +- [ ] All acceptance criteria from the ticket description met +- [ ] PR created (or updated) and linked to Linear issue +- [ ] Workpad updated with completion summary diff --git a/.ai/prompts/investigate.md b/.ai/prompts/investigate.md new file mode 100644 index 0000000..fed5f76 --- /dev/null +++ b/.ai/prompts/investigate.md @@ -0,0 +1,51 @@ +# Investigation Stage + +You are investigating issue **{{ issue.identifier }}**: {{ issue.title }} + +**Current status:** {{ issue.state }} +**Labels:** {{ issue.labels }} +**URL:** {{ issue.url }} + +## Issue description + +{% if issue.description %} +{{ issue.description }} +{% else %} +No description provided. +{% endif %} + +## Objective + +Understand the problem thoroughly before any code is written. Your output is +an investigation summary posted as a Linear comment — not code changes. + +## First run + +1. Read the issue description and any existing Linear comments. +2. Identify the relevant source files — read them, understand the architecture. +3. If the issue is a bug: reproduce it first (run the failing test or repro steps). +4. If the issue is a feature: map out which files/modules need changes. +5. Write a structured investigation summary: + - **Root cause** or **Requirements** (depending on issue type) + - **Affected files** with brief explanation of needed changes + - **Risks or open questions** + - **Proposed approach** (high-level, 3-5 bullet points) +6. Post the summary as a Linear comment titled `## Investigation`. +7. Update the workpad with investigation status. + +## Rework run + +If this is a rework run (the workspace already has investigation content): + +1. Read the review feedback from Linear comments. +2. Read your prior investigation summary. +3. Address the specific feedback — expand analysis, correct mistakes, or + investigate additional areas as requested. +4. Update the `## Investigation` comment with revised findings. +5. Append a rework note to the workpad. + +## Do NOT + +- Write implementation code. +- Create branches or PRs. +- Modify source files (reading is fine). diff --git a/.ai/prompts/merge.md b/.ai/prompts/merge.md new file mode 100644 index 0000000..7ae87ca --- /dev/null +++ b/.ai/prompts/merge.md @@ -0,0 +1,52 @@ +# Merge Stage + +You are merging the approved PR for **{{ issue.identifier }}**: {{ issue.title }} + +**URL:** {{ issue.url }} + +## Objective + +Merge the PR and move the issue to its terminal state. This is a short, +mechanical stage — no new code changes. + +## Process + +1. Find the open PR for this issue: + ``` + gh pr list --head + ``` +2. Verify the PR is approved and CI is passing: + ``` + gh pr view --json reviewDecision,statusCheckRollup + ``` +3. If CI is failing, investigate briefly. If it is a flaky test or transient + failure, re-run the checks. If it is a real failure, post a comment on the + Linear issue and stop. +4. Merge the PR using squash merge: + ``` + gh pr merge --squash --delete-branch + ``` +5. Update the Linear workpad with the merge confirmation. +6. Move the Linear issue to `Done`. + +## Rework run + +If this is a rework run (merge was attempted before but failed): + +1. Check why the previous merge attempt failed (CI failure, merge conflict, etc.). +2. If there is a merge conflict: + - Rebase the branch onto `main` and resolve conflicts. + - Push the updated branch. + - Wait for CI to pass, then merge. +3. If CI failed: + - Read the failure logs. + - If it is a test failure caused by the PR's changes, post details to + Linear and stop (this needs to go back to implementation). + - If it is a flaky or infrastructure issue, re-run and retry the merge. +4. Update the workpad with what happened. + +## Do NOT + +- Make code changes beyond conflict resolution. +- Open new PRs. +- Skip CI checks. diff --git a/.ai/prompts/review.md b/.ai/prompts/review.md new file mode 100644 index 0000000..5b78c04 --- /dev/null +++ b/.ai/prompts/review.md @@ -0,0 +1,64 @@ +# Code Review Stage + +You are an independent code reviewer with NO prior context about this issue. +Review the changes on the current branch compared to `main`. + +**Issue:** {{ issue.identifier }} — {{ issue.title }} +**URL:** {{ issue.url }} + +## Issue description + +{% if issue.description %} +{{ issue.description }} +{% else %} +No description provided. +{% endif %} + +## Objective + +Perform a thorough, adversarial code review. Your job is to find problems +the implementer missed — not to rubber-stamp the PR. + +## Review process + +1. Read the full diff: + ``` + git diff main...HEAD + ``` +2. Read the issue description and any acceptance criteria. +3. For each changed file, read the surrounding code (not just the diff) to + understand the full context. +4. Evaluate: + - **Correctness** — Does the code do what the ticket asks? Edge cases? + - **Quality** — Clean code, no duplication, follows project conventions? + - **Safety** — Error handling, input validation, no security issues? + - **Tests** — Adequate coverage? Do tests actually test the right thing? + - **Performance** — Any obvious regressions or inefficiencies? +5. Run the quality suite yourself to confirm everything passes: + - Type checking + - Linting + - Tests +6. Post your review as a Linear comment titled `## Code Review`: + - List issues found (critical, major, minor) + - Note anything that looks good + - Give an overall assessment: approve, request changes, or flag concerns + +## Rework run + +If this is a rework run (the review stage is being re-run after changes): + +1. Read your prior review from the Linear comments. +2. Read the new commits since your last review: + ``` + git log --oneline main..HEAD + ``` +3. Verify that previously raised issues have been addressed. +4. Check for any new issues introduced by the rework. +5. Post an updated `## Code Review` comment with your revised assessment. + +## Guidelines + +- Be specific: reference file names and line numbers. +- Be constructive: suggest fixes, not just problems. +- Do NOT make code changes yourself — this is a review-only stage. +- Do NOT create or modify branches or PRs. diff --git a/.gitmodules b/.gitmodules index 10b152d..6ac4058 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "vendor/rubyists-homebrew-tap"] path = vendor/rubyists-homebrew-tap url = git@github.com:rubyists/homebrew-tap +[submodule "vendor/stokowski"] + path = vendor/stokowski + url = https://github.com/Sugar-Coffee/stokowski diff --git a/AGENTS.md b/AGENTS.md index e5f24ca..c7c8ba9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,7 @@ and best practices for agents to follow. - Conventional Commits: app/usage-rules.md — enforced by the `commit-msg` and `pre-push` hooks at `githooks/` (run `mix setup` once per clone to activate them). +- Dogfooding & running `lc` locally (no MCP, no escript): app/usage-rules.md diff --git a/app/.gitignore b/app/.gitignore index 07b9c1a..be9c735 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -22,10 +22,6 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). app-*.tar -# The escript built via "mix escript.build" (local dev convenience only - -# the real distribution target is the Burrito release, see Phase 8). -/lc - # Burrito's per-target output binaries (mix release, see documents/phase-8-plan.adoc). /burrito_out/ diff --git a/app/lib/linear_cli/application.ex b/app/lib/linear_cli/application.ex index 48e18a0..bd51ab6 100644 --- a/app/lib/linear_cli/application.ex +++ b/app/lib/linear_cli/application.ex @@ -14,10 +14,10 @@ defmodule LinearCli.Application do # Only the daemon run mode (LINEAR_CLI_DAEMON=true, set by the mix # release's daemon startup) starts the repo + Oban and stays alive. - # Confirmed empirically that the escript boots this whole application on - # every invocation - without this gate, every interactive command would - # also open a database connection and boot Oban's full supervision tree. - # See documents/phase-7-plan.adoc. + # Confirmed empirically that every run mode (Burrito release, `mix run`) + # boots this whole application on every invocation - without this gate, + # every interactive command would also open a database connection and + # boot Oban's full supervision tree. See documents/phase-7-plan.adoc. # # Which repo/engine actually starts is resolved fresh on every boot via # LinearCli.ObanRepo.{repo,oban_engine}/0, not baked in at compile time - @@ -35,19 +35,18 @@ defmodule LinearCli.Application do Supervisor.start_link([repo, {Oban, oban_opts}], opts) end - # `mix escript.build`'s `main_module: LinearCli.CLI` makes the escript - # runtime call `LinearCli.CLI.main/1` itself once boot finishes here - so - # this must NOT also call it, or every interactive command would run - # twice. A Burrito-wrapped release has no such runtime: it boots via - # `-s elixir start_cli`, which only recognizes Elixir's own CLI flags - # (`--help`/`--version`) and otherwise tries to run the first arg as a - # script file (see documents/phase-8-plan.adoc's Burrito verification - - # it only exercised the daemon boot-and-stay-alive path, not this one). - # `LinearCli.CLI.main/2` never reaches this call site as a Burrito - # release, so it has to happen here instead, per Burrito's own - # "Application Entry Point" README section. `running_standalone?/0` - # (checks the `__BURRITO` env var the Zig wrapper sets) is what - # distinguishes that case from escript/`mix run`. + # A Burrito-wrapped release boots via `-s elixir start_cli`, which only + # recognizes Elixir's own CLI flags (`--help`/`--version`) and otherwise + # tries to run the first arg as a script file (see + # documents/phase-8-plan.adoc's Burrito verification - it only exercised + # the daemon boot-and-stay-alive path, not this one). `LinearCli.CLI. + # main/2` never reaches this call site as a Burrito release, so it has + # to happen here instead, per Burrito's own "Application Entry Point" + # README section. `running_standalone?/0` (checks the `__BURRITO` env + # var the Zig wrapper sets) is what distinguishes that case from `mix + # run`, where the caller (a test, an `-e` script, IEx) invokes + # `LinearCli.CLI.main/1` itself - calling it again here would run every + # interactive command twice. defp start_interactive do if Burrito.Util.running_standalone?() do LinearCli.CLI.main(Burrito.Util.Args.argv()) diff --git a/app/lib/linear_cli/oban_repo.ex b/app/lib/linear_cli/oban_repo.ex index b4e5530..b88439c 100644 --- a/app/lib/linear_cli/oban_repo.ex +++ b/app/lib/linear_cli/oban_repo.ex @@ -3,8 +3,8 @@ defmodule LinearCli.ObanRepo do Resolves which Ecto repo (and matching Oban engine) actually backs Oban's job table, at *runtime* - `LINEAR_CLI_DB_ADAPTER=sqlite|postgres` (default `sqlite`), read fresh every time the daemon boots (see - `LinearCli.Application`). This is what lets one compiled escript/release - run against either backend on whatever machine it's deployed to, with no + `LinearCli.Application`). This is what lets one compiled release run + against either backend on whatever machine it's deployed to, with no rebuild. The *adapter* itself is still necessarily fixed per `Ecto.Repo` module - diff --git a/app/mix.exs b/app/mix.exs index 24ce92c..a5bfd9a 100644 --- a/app/mix.exs +++ b/app/mix.exs @@ -12,15 +12,10 @@ defmodule LinearCli.MixProject do deps: deps(), consolidate_protocols: Mix.env() != :dev, usage_rules: usage_rules(), - escript: escript(), releases: releases() ] end - defp escript do - [main_module: LinearCli.CLI, name: "lc"] - end - # Burrito-wrapped release, both the interactive CLI and (with # LINEAR_CLI_DAEMON=true) the daemon - one binary, not two build # artifacts. Targets and their host-compatibility verified against diff --git a/app/usage-rules.md b/app/usage-rules.md index 17ea9a2..c277cb4 100644 --- a/app/usage-rules.md +++ b/app/usage-rules.md @@ -16,3 +16,20 @@ - Enforced in CI across a whole PR's commit range by the same `ci/conventional_commits.sh` the `pre-push` hook uses (skips GitHub's own auto-generated update-branch merge commits). + +## Dogfooding: use `lc`, not a Linear MCP server or skill + +- When working in this repo, read or write Linear data through this + repo's own `lc` CLI — never a Linear MCP server or other + Linear-integration skill. `lc` is the codebase under development; + routing around it means it never gets exercised. +- There's no escript build (removed - NIF-backed deps like `exqlite` + can't load from inside an escript archive, so it never actually + worked once `exqlite` was added). Without a full `mix release lc` + build, invoke `lc` from `app/` via `mix run`: + + mix run -e 'LinearCli.CLI.main(["issue", "list"])' + + `main/2`'s default `halt` (`System.halt/1`) is fine to leave as-is — + only error paths call it, so a successful command just returns and + `mix run` exits 0 normally with real output and real exit codes. diff --git a/lib/mix/tasks/stokowski.ex b/lib/mix/tasks/stokowski.ex new file mode 100644 index 0000000..03231ae --- /dev/null +++ b/lib/mix/tasks/stokowski.ex @@ -0,0 +1,77 @@ +defmodule Mix.Tasks.Stokowski do + @shortdoc "Validates this repo's workflow.yaml is safe for a Stokowski session" + + @moduledoc """ + #{@shortdoc}. + + mix stokowski + + Doesn't launch Stokowski itself yet - for now this just checks the + root `workflow.yaml` is safe to use: + + * it exists + * it's covered by `.gitignore` (it can hold a live Linear API key, + so it must never be trackable) + * its `tracker.api_key`, if set at all, is a `"$VAR"` env-var + reference rather than a bare literal key (omitting the key + entirely is also fine - Stokowski then falls back to the + `LINEAR_API_KEY` env var directly) + + See `vendor/stokowski/README.md` for what the file needs to contain; + actually starting a session is a follow-up step. + """ + + use Mix.Task + + @impl Mix.Task + def run(_argv) do + workflow = Path.expand("workflow.yaml") + + unless File.exists?(workflow) do + Mix.raise( + "No workflow.yaml at #{workflow} - see vendor/stokowski/README.md's setup guide" + ) + end + + unless gitignored?(workflow) do + Mix.raise( + "#{workflow} is not gitignored - it can hold a live Linear API key, add it to .gitignore before continuing" + ) + end + + case api_key(workflow) do + nil -> + Mix.shell().info( + "No tracker.api_key set - Stokowski will fall back to the LINEAR_API_KEY env var" + ) + + {:env_ref, var} -> + Mix.shell().info("tracker.api_key references $#{var} - good") + + {:literal, _value} -> + Mix.raise( + "tracker.api_key in #{workflow} is a bare literal key - use \"$LINEAR_API_KEY\" (or omit the key entirely) instead" + ) + end + + Mix.shell().info("#{workflow} looks safe to use.") + :ok + end + + defp gitignored?(path) do + match?({_, 0}, System.cmd("git", ["check-ignore", "-q", path], stderr_to_stdout: true)) + end + + defp api_key(path) do + path + |> File.read!() + |> String.split("\n") + |> Enum.find_value(fn line -> + case Regex.run(~r/^\s*api_key:\s*"?([^"\s#]+)/, line) do + [_, "$" <> var] -> {:env_ref, var} + [_, value] -> {:literal, value} + nil -> nil + end + end) + end +end diff --git a/mise.toml b/mise.toml index 58044c2..079fd95 100644 --- a/mise.toml +++ b/mise.toml @@ -6,3 +6,4 @@ # works. See documents/phase-8-plan.adoc. erlang = "29.0.3" elixir = "1.20.3" +python = "3.14.1" diff --git a/test/mix/tasks/stokowski_test.exs b/test/mix/tasks/stokowski_test.exs new file mode 100644 index 0000000..cc3bc99 --- /dev/null +++ b/test/mix/tasks/stokowski_test.exs @@ -0,0 +1,11 @@ +defmodule Mix.Tasks.StokowskiTest do + use ExUnit.Case, async: true + + test "raises when no workflow.yaml is found" do + File.cd!(System.tmp_dir!(), fn -> + assert_raise Mix.Error, ~r/No workflow\.yaml at/, fn -> + Mix.Tasks.Stokowski.run([]) + end + end) + end +end diff --git a/vendor/rubyists-homebrew-tap b/vendor/rubyists-homebrew-tap index ab0aae0..7bb1c1d 160000 --- a/vendor/rubyists-homebrew-tap +++ b/vendor/rubyists-homebrew-tap @@ -1 +1 @@ -Subproject commit ab0aae00839b2abe2156a8e6ab5852eaed749bf5 +Subproject commit 7bb1c1d7d4e1104bf0e3c86d0f0c365a4b55c17b diff --git a/vendor/stokowski b/vendor/stokowski new file mode 160000 index 0000000..6e51bdf --- /dev/null +++ b/vendor/stokowski @@ -0,0 +1 @@ +Subproject commit 6e51bdf26c8cf6206893beb7c01b71297539469d diff --git a/workflow.yaml b/workflow.yaml new file mode 100644 index 0000000..2b35472 --- /dev/null +++ b/workflow.yaml @@ -0,0 +1,260 @@ +# ============================================================================= +# Stokowski — Example State Machine Workflow +# ============================================================================= +# +# Copy this file to workflow.yaml and customise for your project. +# Stokowski re-reads this file on every poll tick — changes take effect +# without restart. +# +# Prompt files live in the prompts/ directory. Each agent state references +# its prompt via the `prompt` field. A global prompt (shared preamble) is +# injected before the stage-specific prompt when set. +# +# Linear states mapping: +# active → the "working" state for agent stages +# review → human review / PR review state +# gate_approved → human approved a gate (triggers transition) +# rework → human requested changes (triggers rework_to) +# terminal → list of done/closed states +# +# State types: +# agent — Stokowski dispatches Claude Code in this state +# gate — pauses for human review; approve → next, rework → rework_to +# terminal — issue is finished; workspace cleaned up +# +# ----------------------------------------------------------------------------- +# Multi-project mode (optional — single-project YAML below still works as-is) +# ----------------------------------------------------------------------------- +# +# To monitor multiple Linear projects from one Stokowski process, replace the +# top-level `tracker / workspace / hooks / prompts / states` blocks with a +# `projects:` list. Each entry has its own tracker/workspace/hooks/prompts/ +# state machine. Top-level `polling`, `agent`, `claude`, `linear_states`, and +# `server` stay global and act as defaults each project can override. +# +# agent: +# max_concurrent_agents: 8 # global pool across all projects +# max_concurrent_per_project: # optional fairness caps +# synced-sport: 6 +# client-site: 2 +# +# projects: +# - name: synced-sport # required, used in dashboard + `p` menu +# tracker: +# kind: linear +# project_slug: "abc123def456" +# api_key: "$LINEAR_API_KEY" +# workspace: +# root: ~/.local/share/stokowski/workspaces/synced-sport +# hooks: { after_create: "git clone git@github.com:org/synced-sport.git ." } +# prompts: { global_prompt: prompts/synced-sport/global.md } +# states: { ... } # state machine, same as single-project +# +# - name: client-site +# paused: true # start paused; toggle with `p` key +# tracker: +# kind: linear +# project_slug: "def456abc789" +# api_key: "$LINEAR_API_KEY" +# workspace: +# root: ~/.local/share/stokowski/workspaces/client-site +# hooks: { after_create: "git clone git@github.com:org/client-site.git ." } +# prompts: { global_prompt: prompts/client-site/global.md } +# claude: { model: claude-opus-4-7 } # per-project override +# max_concurrent: 2 # per-project cap (also via agent map above) +# states: { ... } +# +# In the terminal, `p` opens a numbered pause menu; in the web dashboard, each +# project tile gets a Pause/Resume button and a per-project filter dropdown. +# ============================================================================= + +# --------------------------------------------------------------------------- +# Tracker +# --------------------------------------------------------------------------- +tracker: + kind: linear + project_slug: "94a76f2ac65f" # hex slugId from your Linear project URL + # api_key: "$LINEAR_API_KEY" # your Linear API key — agents inherit this + +# --------------------------------------------------------------------------- +# Linear state name mapping +# --------------------------------------------------------------------------- +# These map Stokowski's internal lifecycle roles to your Linear state names. +# You can rename values to match your Linear setup (e.g. todo: "Ready"), +# but all six roles are required — removing or reordering them will break +# the dispatch and gate protocol. +linear_states: + todo: "Todo" # issues picked up from this state + active: "In Progress" # moved here automatically when agent starts + review: "Human Review" # agent pauses here at a gate for human review + gate_approved: "Gate Approved" # human approved — agent advances to next state + rework: "Rework" # human requested changes — agent re-enters rework target + terminal: # issues in these states stop any running agent + - Done + - Closed + - Cancelled + - Canceled + - Duplicate + +# --------------------------------------------------------------------------- +# Polling +# --------------------------------------------------------------------------- +polling: + interval_ms: 15000 # 15 seconds between ticks + +# --------------------------------------------------------------------------- +# Workspace +# --------------------------------------------------------------------------- +workspace: + root: ~/rubyists/code/stokowski-workspaces + +# --------------------------------------------------------------------------- +# Hooks — shell scripts run in the workspace directory +# --------------------------------------------------------------------------- +hooks: + after_create: | + git clone --depth 1 --recursive git@github.com:rubyists/linear-cli . + mix setup + before_run: | + git fetch origin main + git rebase origin/main 2>/dev/null || git rebase --abort + timeout_ms: 120000 + +# --------------------------------------------------------------------------- +# Claude defaults (inherited by all agent states unless overridden) +# States using runner: codex will use the Codex CLI instead. +# --------------------------------------------------------------------------- +claude: + permission_mode: auto + model: claude-sonnet-4-6 + max_turns: 20 + turn_timeout_ms: 3600000 # 1 hour per turn + stall_timeout_ms: 300000 # 5 minutes with no output → kill + +# --------------------------------------------------------------------------- +# Agent concurrency +# --------------------------------------------------------------------------- +agent: + max_concurrent_agents: 4 + max_retry_backoff_ms: 300000 # 5-minute cap on exponential backoff + max_concurrent_agents_by_state: + investigate: 2 # allow 2 concurrent investigations + implement: 2 + code-review: 1 # serial reviews prevent merge conflicts + +# --------------------------------------------------------------------------- +# Prompts +# --------------------------------------------------------------------------- +prompts: + global_prompt: .ai/prompts/global.md + +# --------------------------------------------------------------------------- +# Server (optional web dashboard) +# --------------------------------------------------------------------------- +server: + host: 127.0.0.1 + port: 4200 + +# --------------------------------------------------------------------------- +# State machine +# --------------------------------------------------------------------------- +# +# Order matters: the first agent state is the entry state for new issues. +# +# Transitions fire automatically on agent success or human action: +# - Agent finishes successfully → "complete" transition +# - Human approves a gate → "approve" transition +# - Human requests rework → rework_to target +# +states: + + # ── 1. Investigate ────────────────────────────────────────────────────── + # Read the codebase, understand the problem, post an investigation summary. + # Uses Opus for deeper reasoning. Low turn count — investigation only. + investigate: + type: agent + prompt: prompts/investigate.md + linear_state: active + model: claude-opus-4-6 + max_turns: 8 + session: inherit + transitions: + complete: research-review + + # ── 2. Research Review (gate) ─────────────────────────────────────────── + # Human reviews the investigation before implementation starts. + # Rework loops back to investigate. + research-review: + type: gate + linear_state: review + rework_to: investigate + max_rework: 3 + transitions: + approve: implement + + # ── 3. Implement ──────────────────────────────────────────────────────── + # Write code, create branch, run tests, open PR. + # Uses Sonnet for fast iteration. Higher turn count for complex work. + implement: + type: agent + prompt: prompts/implement.md + linear_state: active + model: claude-sonnet-4-6 + max_turns: 30 + session: inherit + transitions: + complete: implementation-review + + # ── 4. Implementation Review (gate) ───────────────────────────────────── + # Human reviews the PR before automated code review. + # Rework loops back to implement. + implementation-review: + type: gate + linear_state: review + rework_to: implement + max_rework: 5 + transitions: + approve: code-review + + # ── 5. Code Review ───────────────────────────────────────────────────── + # Automated adversarial review: fresh session, no prior context. + # Opus reviews the diff and posts findings. + # Tip: set runner: codex here to get a second-opinion from a different + # provider — different states can use different runners in the same run. + code-review: + type: agent + prompt: prompts/review.md + linear_state: active + runner: claude # "claude" (default) or "codex" + model: claude-opus-4-6 + max_turns: 10 + session: fresh # no prior context — adversarial review + transitions: + complete: merge-review + + # ── 6. Merge Review (gate) ────────────────────────────────────────────── + # Human reviews the code review findings and decides to merge or rework. + # Rework goes back to implement (not code-review). + merge-review: + type: gate + linear_state: review + rework_to: implement + max_rework: 5 + transitions: + approve: merge + + # ── 7. Merge ──────────────────────────────────────────────────────────── + # Merge the PR and clean up. Short turn count — just the merge. + merge: + type: agent + prompt: prompts/merge.md + linear_state: active + max_turns: 5 + session: inherit + transitions: + complete: done + + # ── 8. Done (terminal) ───────────────────────────────────────────────── + done: + type: terminal + linear_state: terminal From b502289b6c2af535a6c5bda086122618cc7d8e1c Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Aug 2026 13:02:23 -0400 Subject: [PATCH 2/4] style: format lib/mix/tasks/stokowski.ex per mix format --- lib/mix/tasks/stokowski.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/mix/tasks/stokowski.ex b/lib/mix/tasks/stokowski.ex index 03231ae..826e642 100644 --- a/lib/mix/tasks/stokowski.ex +++ b/lib/mix/tasks/stokowski.ex @@ -28,9 +28,7 @@ defmodule Mix.Tasks.Stokowski do workflow = Path.expand("workflow.yaml") unless File.exists?(workflow) do - Mix.raise( - "No workflow.yaml at #{workflow} - see vendor/stokowski/README.md's setup guide" - ) + Mix.raise("No workflow.yaml at #{workflow} - see vendor/stokowski/README.md's setup guide") end unless gitignored?(workflow) do From dcbdabf7674faa3f23e6aff8037ea876236c269a Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Aug 2026 15:32:24 -0400 Subject: [PATCH 3/4] fix: drop the gitignore requirement from mix stokowski workflow.yaml is meant to be tracked in this repo, not gitignored - the moduledoc and error messages said otherwise, contradicting what this PR actually does. The task now only checks tracker.api_key isn't a bare literal (a best-effort guard against a leaked key), not that the file stays untracked. --- lib/mix/tasks/stokowski.ex | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/mix/tasks/stokowski.ex b/lib/mix/tasks/stokowski.ex index 826e642..9131906 100644 --- a/lib/mix/tasks/stokowski.ex +++ b/lib/mix/tasks/stokowski.ex @@ -7,16 +7,18 @@ defmodule Mix.Tasks.Stokowski do mix stokowski Doesn't launch Stokowski itself yet - for now this just checks the - root `workflow.yaml` is safe to use: + root `workflow.yaml` is safe to commit: * it exists - * it's covered by `.gitignore` (it can hold a live Linear API key, - so it must never be trackable) * its `tracker.api_key`, if set at all, is a `"$VAR"` env-var reference rather than a bare literal key (omitting the key entirely is also fine - Stokowski then falls back to the `LINEAR_API_KEY` env var directly) + `workflow.yaml` is meant to be tracked in this repo, not gitignored - + this is a best-effort guard against a literal API key ever landing in + it, not a requirement that the file stay untracked. + See `vendor/stokowski/README.md` for what the file needs to contain; actually starting a session is a follow-up step. """ @@ -31,12 +33,6 @@ defmodule Mix.Tasks.Stokowski do Mix.raise("No workflow.yaml at #{workflow} - see vendor/stokowski/README.md's setup guide") end - unless gitignored?(workflow) do - Mix.raise( - "#{workflow} is not gitignored - it can hold a live Linear API key, add it to .gitignore before continuing" - ) - end - case api_key(workflow) do nil -> Mix.shell().info( @@ -56,10 +52,6 @@ defmodule Mix.Tasks.Stokowski do :ok end - defp gitignored?(path) do - match?({_, 0}, System.cmd("git", ["check-ignore", "-q", path], stderr_to_stdout: true)) - end - defp api_key(path) do path |> File.read!() From 82877f0f011b97db98dba905f08d2553d89b0743 Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Thu, 13 Aug 2026 15:52:07 -0400 Subject: [PATCH 4/4] fix: address PR review comments on the stokowski vendoring work - Fix four state definitions in workflow.yaml pointing at prompts/*.md - the committed prompts actually live at .ai/prompts/*.md, so Stokowski couldn't have loaded any of them as written. - Reword workflow.yaml's header comments: it's the repo's own checked-in config now, not a template to copy, and prompts live under .ai/prompts/, not prompts/. - Replace workspace.root's personal path with a neutral default shared contributors can use out of the box. - Add test coverage for the literal-api_key rejection path (previously only the missing-file case was covered). Not applying Copilot's suggestion to switch workflow.yaml's clone hook from SSH to HTTPS - SSH with a primed agent failing loudly is preferable to HTTPS quietly working via cached creds on disk. --- test/mix/tasks/stokowski_test.exs | 29 ++++++++++++++++++++++++++++- workflow.yaml | 22 +++++++++++----------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/test/mix/tasks/stokowski_test.exs b/test/mix/tasks/stokowski_test.exs index cc3bc99..e3f1be3 100644 --- a/test/mix/tasks/stokowski_test.exs +++ b/test/mix/tasks/stokowski_test.exs @@ -2,10 +2,37 @@ defmodule Mix.Tasks.StokowskiTest do use ExUnit.Case, async: true test "raises when no workflow.yaml is found" do - File.cd!(System.tmp_dir!(), fn -> + in_tmp_dir(fn -> assert_raise Mix.Error, ~r/No workflow\.yaml at/, fn -> Mix.Tasks.Stokowski.run([]) end end) end + + test "raises when tracker.api_key is a bare literal" do + in_tmp_dir(fn -> + File.write!("workflow.yaml", """ + tracker: + api_key: "lin_api_totally_real" + """) + + assert_raise Mix.Error, ~r/is a bare literal key/, fn -> + Mix.Tasks.Stokowski.run([]) + end + end) + end + + # Each test gets its own directory rather than sharing System.tmp_dir!() + # directly - both tests run async and would otherwise race on the same + # workflow.yaml. + defp in_tmp_dir(fun) do + dir = Path.join(System.tmp_dir!(), "stokowski_test_#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + + try do + File.cd!(dir, fun) + after + File.rm_rf!(dir) + end + end end diff --git a/workflow.yaml b/workflow.yaml index 2b35472..f84b365 100644 --- a/workflow.yaml +++ b/workflow.yaml @@ -2,13 +2,13 @@ # Stokowski — Example State Machine Workflow # ============================================================================= # -# Copy this file to workflow.yaml and customise for your project. -# Stokowski re-reads this file on every poll tick — changes take effect -# without restart. +# This is linear-cli's own checked-in workflow config, not a template to +# copy elsewhere. Stokowski re-reads this file on every poll tick — changes +# take effect without restart. # -# Prompt files live in the prompts/ directory. Each agent state references -# its prompt via the `prompt` field. A global prompt (shared preamble) is -# injected before the stage-specific prompt when set. +# Prompt files live in the .ai/prompts/ directory. Each agent state +# references its prompt via the `prompt` field. A global prompt (shared +# preamble) is injected before the stage-specific prompt when set. # # Linear states mapping: # active → the "working" state for agent stages @@ -106,7 +106,7 @@ polling: # Workspace # --------------------------------------------------------------------------- workspace: - root: ~/rubyists/code/stokowski-workspaces + root: ~/.local/share/stokowski/workspaces/linear-cli # --------------------------------------------------------------------------- # Hooks — shell scripts run in the workspace directory @@ -173,7 +173,7 @@ states: # Uses Opus for deeper reasoning. Low turn count — investigation only. investigate: type: agent - prompt: prompts/investigate.md + prompt: .ai/prompts/investigate.md linear_state: active model: claude-opus-4-6 max_turns: 8 @@ -197,7 +197,7 @@ states: # Uses Sonnet for fast iteration. Higher turn count for complex work. implement: type: agent - prompt: prompts/implement.md + prompt: .ai/prompts/implement.md linear_state: active model: claude-sonnet-4-6 max_turns: 30 @@ -223,7 +223,7 @@ states: # provider — different states can use different runners in the same run. code-review: type: agent - prompt: prompts/review.md + prompt: .ai/prompts/review.md linear_state: active runner: claude # "claude" (default) or "codex" model: claude-opus-4-6 @@ -247,7 +247,7 @@ states: # Merge the PR and clean up. Short turn count — just the merge. merge: type: agent - prompt: prompts/merge.md + prompt: .ai/prompts/merge.md linear_state: active max_turns: 5 session: inherit