diff --git a/.ai/prompts/global.md b/.ai/prompts/global.md index 88d34d7..83469e9 100644 --- a/.ai/prompts/global.md +++ b/.ai/prompts/global.md @@ -27,13 +27,24 @@ Before starting any implementation work: 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 +## Linear progress updates -Use a single Linear comment as a persistent workpad: +Post a new Linear comment for each milestone of your work — investigation +findings, implementation decisions, results, guidance for the next +stage, and so on. Do not try to maintain or find a single running +comment to update: -- 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. + mix lc issue comment --body-file + +- Write the comment's full content to a file first, then pass its + path — never build a multi-line comment as an inline shell argument. +- Each comment should stand on its own: describe only this step's + findings, decisions, and results, not the whole history. Read prior + comments for context (`mix lc issue ls --full `); post a new + one for what's new, don't try to edit an old one. +- Always only use `mix lc` to interact with Linear — never call the + Linear API directly (curl, GraphQL, or otherwise). If `mix lc` is + broken, log that error and stop processing. ## Rework awareness @@ -43,4 +54,5 @@ 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) +- Prior progress comments (read them for context; post a new comment for + this run rather than editing an old one) diff --git a/.ai/prompts/implement.md b/.ai/prompts/implement.md index 4fa883e..7ca545a 100644 --- a/.ai/prompts/implement.md +++ b/.ai/prompts/implement.md @@ -18,6 +18,11 @@ No description provided. Implement the solution, create a PR, and ensure it passes all quality checks. +## Rule + +Always sign git commits. If a gpg-agent is not available with a signing key, +stop and note that on the linear issue, do not create an unsigned commit. + ## First run 1. Read the investigation summary from the Linear comments. @@ -36,7 +41,7 @@ Implement the solution, create a PR, and ensure it passes all quality checks. 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. +9. Post a Linear comment with: what was done, what was tested, any known limitations. ## Rework run @@ -57,7 +62,7 @@ If this is a rework run (a branch and PR already exist): - Which review comments were addressed - What was modified - Any decisions or trade-offs -7. Append a rework section to the Linear workpad. +7. Post a Linear comment summarising the rework. ## Quality bar @@ -68,4 +73,4 @@ Before finishing, verify: - [ ] 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 +- [ ] Linear comment posted with a completion summary diff --git a/.ai/prompts/investigate.md b/.ai/prompts/investigate.md index fed5f76..6e0d85a 100644 --- a/.ai/prompts/investigate.md +++ b/.ai/prompts/investigate.md @@ -31,7 +31,6 @@ an investigation summary posted as a Linear comment — not code 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 @@ -41,8 +40,8 @@ If this is a rework run (the workspace already has investigation content): 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. +4. Post a new Linear comment titled `## Investigation (rework)` with the + revised findings — do not try to edit the prior comment. ## Do NOT diff --git a/.ai/prompts/merge.md b/.ai/prompts/merge.md index 7ae87ca..0f6053b 100644 --- a/.ai/prompts/merge.md +++ b/.ai/prompts/merge.md @@ -26,8 +26,9 @@ mechanical stage — no new code changes. ``` gh pr merge --squash --delete-branch ``` -5. Update the Linear workpad with the merge confirmation. -6. Move the Linear issue to `Done`. +5. Always use `mix lc issue` to interact with Linear issues +6. Post a Linear comment with the merge confirmation. +7. Move the Linear issue to `Done`. ## Rework run @@ -43,10 +44,11 @@ If this is a rework run (merge was attempted before but failed): - 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. +4. Post a Linear comment with what happened. ## Do NOT - Make code changes beyond conflict resolution. - Open new PRs. - Skip CI checks. +- Use anything other than `mix lc` to interact with Linear diff --git a/.ai/prompts/review.md b/.ai/prompts/review.md index 5b78c04..00aa7ea 100644 --- a/.ai/prompts/review.md +++ b/.ai/prompts/review.md @@ -39,6 +39,7 @@ the implementer missed — not to rubber-stamp the PR. - Linting - Tests 6. Post your review as a Linear comment titled `## Code Review`: + - Always only use `mix lc issue` to interact with Linear - List issues found (critical, major, minor) - Note anything that looks good - Give an overall assessment: approve, request changes, or flag concerns diff --git a/.gitmodules b/.gitmodules index a0037d1..b3d4418 100644 --- a/.gitmodules +++ b/.gitmodules @@ -27,4 +27,4 @@ url = git@github.com:fuelen/owl [submodule "vendor/stokowski"] path = vendor/stokowski - url = https://github.com/Sugar-Coffee/stokowski + url = git@github.com:bougyman/stokowski diff --git a/AGENTS.md b/AGENTS.md index 9289add..8679014 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,9 +14,31 @@ and best practices for agents to follow. - Phase 9 Plan: documents/phase-9-plan.adoc - Phase 10 Plan: documents/phase-10-plan.adoc - Phase 11 Plan: documents/phase-11-plan.adoc +- Phase 12 Plan: documents/phase-12-plan.adoc +- Phase 13 Plan: documents/phase-13-plan.adoc ## Project Structure +### Linear Team and Project + +The `Linear CLI` project of the "Elixir Traitors" (`EXT`) team on the +https://linear.app/the-rubyists[Rubyists Linear Organization] manages +the project roadmap and issue tracking. + +#### Rules for Interacting with Linear + +Always only use `mix lc` to interact with linear.app. Do not look for an +mcp or any other way to interact with issues or the project. + +`mix lc --help` +`mix lc issue --help` +`mix lc issue list --help` +`mix lc issue create --help` +`mix lc issue status --help` +`mix lc issue comment --help` + +This should be enough to get started + ### Repository layout This repo uses two Mix projects side-by-side, not nested: diff --git a/Readme.adoc b/Readme.adoc index 5c01927..517ad81 100644 --- a/Readme.adoc +++ b/Readme.adoc @@ -160,6 +160,7 @@ so you don't have to type the full name every time: |`issue assign` |`a` |`issue move` |`m`, `mv` |`issue update` |`u` +|`issue comment` | |`issue pr` |`pull-request` |`team list` |`l`, `ls` |`project list` |`l`, `ls` @@ -289,6 +290,22 @@ $ lcomment CRY-1234 CRY-3 <5> <4> Opens your editor for the comment (use `-` to prompt) <5> Always prompts for a comment (`lcomment` is a wrapper for `lc issue update --comment -`) +===== Add a comment from a file (`issue comment`) + +Same plain create as `issue update --comment`/`lcomment`, plus +`--body-file` — the way to supply a large multi-line body without +building it as a single shell argument. + +[source,sh] +---- +$ lc issue comment CRY-1234 -m "Here is a comment" <1> +$ lc issue comment CRY-1234 --body-file notes.md <2> +$ lc issue comment CRY-1234 --body-file - <3> +---- +<1> Plain create, same as `issue update --comment` +<2> Reads the body from a file +<3> Reads the body from stdin + ===== Close one or many issues Use `--status`/`-s` to choose the completed or cancelled workflow state by diff --git a/app/lib/linear_cli/cli.ex b/app/lib/linear_cli/cli.ex index 0a4cc87..756683b 100644 --- a/app/lib/linear_cli/cli.ex +++ b/app/lib/linear_cli/cli.ex @@ -212,6 +212,10 @@ defmodule LinearCli.CLI do defp dispatch([:issue, :pr], result, halt), do: run(&Commands.issue_pr/1, result, halt) defp dispatch([:issue, :move], result, halt), do: run(&Commands.issue_move/1, result, halt) + + defp dispatch([:issue, :comment], result, halt), + do: run(&Commands.issue_comment/1, result, halt) + defp dispatch([:issue, :take], result, halt), do: run(&Commands.issue_take/1, result, halt) defp dispatch([:issue, :status], result, halt), do: run(&Commands.issue_status/1, result, halt) defp dispatch([:issue, :update], result, halt), do: run(&Commands.issue_update/1, result, halt) @@ -738,6 +742,24 @@ defmodule LinearCli.CLI do description: [long: "--description", help: "The description of the PR"] ] ], + comment: [ + name: "comment", + about: "Add a comment to an issue", + args: [ + issue_id: [value_name: "ISSUE_ID", help: "The Issue (i.e. CRY-1)", required: true] + ], + options: [ + comment: [ + short: "-m", + long: "--comment", + help: "Comment text. - opens an editor" + ], + body_file: [ + long: "--body-file", + help: "Read the comment body from this file (- for stdin) instead of --comment" + ] + ] + ], status: [ name: "status", about: "Change workflow state (ISSUE_ID...)", diff --git a/app/lib/linear_cli/cli/commands.ex b/app/lib/linear_cli/cli/commands.ex index 45204fa..8bf1917 100644 --- a/app/lib/linear_cli/cli/commands.ex +++ b/app/lib/linear_cli/cli/commands.ex @@ -519,6 +519,55 @@ defmodule LinearCli.CLI.Commands do end end + @doc """ + Adds a comment to a single issue. + + `--comment`/`-m` and `--body-file` are mutually exclusive. `--body-file` + reads the body from a file (`-` for stdin) - the way to supply a large + multi-line body without building it as a single shell argument, which + is what `--comment`, going through + `LinearCli.CLI.WhatFor.comment_for/2`'s prompt/editor resolution, does + not protect against. Without either option, `comment_for/2`'s existing + behavior applies (prompt, or open an editor for `-`). + + Calls `Linear.add_comment/2` directly rather than + `LinearCli.CLI.IssueHelpers.issue_comment/2` so the confirmation can be + suppressed under `--output json` - matching how `print_move_results/3` + suppresses its own confirmation for `issue move --output json`. + + New in this port - Ruby has no equivalent. + """ + @spec issue_comment(Optimus.ParseResult.t()) :: :ok | {:error, term()} + def issue_comment(%{args: %{issue_id: issue_id}, options: options}) do + with :ok <- validate_comment_options(options), + {:ok, comment_text} <- resolve_comment_body(options), + {:ok, [issue]} <- Linear.issues(%{ids: [IssueHelpers.expand_issue_id(issue_id)]}), + body = WhatFor.comment_for(issue, comment_text), + {:ok, comment} <- Linear.add_comment(issue.identifier, body) do + unless options.output == "json", do: Prompt.ok("Comment added to #{issue.identifier}") + Display.show(comment, %{output: options.output}) + :ok + end + end + + defp validate_comment_options(%{comment: comment, body_file: body_file}) + when not is_nil(comment) and not is_nil(body_file) do + {:error, {:smells_bad, "give --comment or --body-file, not both"}} + end + + defp validate_comment_options(_options), do: :ok + + defp resolve_comment_body(%{body_file: nil, comment: comment}), do: {:ok, comment} + defp resolve_comment_body(%{body_file: "-"}), do: {:ok, read_stdin()} + defp resolve_comment_body(%{body_file: path}), do: File.read(path) + + defp read_stdin do + case IO.read(:stdio, :eof) do + :eof -> "" + data -> data + end + end + @doc """ Moves issues to a target project. diff --git a/app/lib/linear_cli/cli/display.ex b/app/lib/linear_cli/cli/display.ex index d5a8538..62f3415 100644 --- a/app/lib/linear_cli/cli/display.ex +++ b/app/lib/linear_cli/cli/display.ex @@ -7,7 +7,7 @@ defmodule LinearCli.CLI.Display do own `#to_s`/`#full`/`#display` methods. """ - alias LinearCli.Linear.{Issue, Project, ProjectUpdate, Team, User} + alias LinearCli.Linear.{Comment, Issue, Project, ProjectUpdate, Team, User} alias LinearCli.Profiles.Profile @ash_internal_fields ~w(__meta__ __metadata__ __order__ __lateral_join_source__ aggregates calculations)a @@ -37,6 +37,15 @@ defmodule LinearCli.CLI.Display do IO.puts("Posted#{health}: #{update.url}") end + # New in this port - Ruby has no equivalent (no bare `Comment` command + # existed to display one). `LinearCli.CLI.IssueHelpers.issue_comment/2`/ + # `upsert_comment/4` already print a "Comment added to.../updated on..." + # confirmation via `Prompt.ok/1` before this runs, so this only needs to + # add the one thing that isn't in that line: a link to the comment. + defp puts_text(%Comment{} = comment, _opts) do + IO.puts(comment.url || "(no URL returned)") + end + defp puts_text(%Profile{} = profile, _opts) do marker = if profile.active, do: "* ", else: " " diff --git a/app/mix.lock b/app/mix.lock index 3130ce0..e0a21db 100644 --- a/app/mix.lock +++ b/app/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.31.3", "b36672bff745eadd52265d8b0a303f98ba26a31173f4116ebd2df7be0cbafeff", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c105da2080475d114908c32e70970e69c857a9068a577a4e99dbf2801baa11e"}, + "ash": {:hex, :ash, "3.32.3", "a6390b9f6497458f4575220cd507b6b5aae52c43bba0cc057aa3fa96e44ef416", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4eebbd3fa7dab05d0aab9c7552155c07e746b18c8dc1cf5ed2f62195cea87f3d"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "burrito": {:hex, :burrito, "1.6.0", "7af0a75f11680e8a6e9c01370c9af51cb9d0e15b3226eddf4f438dbc68570520", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, ">= 0.5.0", [hex: :req, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.2.0 or ~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "e636a00b032c45a69ff755d9fc53fa5fdc9e1d21bdbd229075fe4a15b05355fe"}, "castore": {:hex, :castore, "1.0.21", "0a0e8330dc267a40a3b7ad86d39302764bb71758172904e6a59d5ad6443ce307", [:mix], [], "hexpm", "e42e22723e25dbd46876d056a03f685513d6e98f6b5e555dc551321decd76c5c"}, @@ -45,7 +45,7 @@ "protobuf": {:hex, :protobuf, "0.16.1", "7bf0c3e6b24fd297e2e3722ecb985c3900bcc55d004b7faf0737e248c3e64691", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "97fb9f1fe48c38c4ebfc0f8a7a20adb22b2491bbfbdb192fab9a6821d5d604e1"}, "purl": {:hex, :purl, "0.3.0", "b6e2f792e944007ed474299e56ea43ea4819f064a4a4f67e4153e1ba02d066de", [:mix, :rebar3], [{:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "158cf2c0533517fe0d68752bd790392f065c5ffdfbde38524890066ebe640302"}, "reactor": {:hex, :reactor, "1.0.6", "546a87255693bcee99451d022cc86927161cdb527f5216d4dcac2b31e08eb122", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:multigraph, "~> 0.16.1-mg.2", [hex: :multigraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "e3f8fd8e870c2b011316ca2ac422bb4bc710cac9f02c578f8526f7a6348d932b"}, - "req": {:hex, :req, "0.7.2", "364eae2e5f5c984f2dac6d71c07f8c8c89ce0bc49c4d746dacb7a306823020de", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "c9cdfa276b05d8db2a27fda5d233e6858b764d47189d76cbb186e130a871ae0b"}, + "req": {:hex, :req, "0.7.4", "23e9ffec17de032a46a4b15ed65c09793893bf4a7c680f4bbf6227fce6bdf74d", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "4b192d63253e8dcc6221ef992ea9ebef7d3555166e8423aa5b553e86bc3c69a2"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, "sbom": {:hex, :sbom, "0.10.0", "b99be5407bc196d0ad71b8061126a67aae46dc3bfaa852b4c1c04645dd1ad984", [:mix], [{:hex_core, "~> 0.15.0", [hex: :hex_core, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:optimus, "~> 0.6.1", [hex: :optimus, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.16.0", [hex: :protobuf, repo: "hexpm", optional: false]}, {:purl, "~> 0.3.0", [hex: :purl, repo: "hexpm", optional: false]}], "hexpm", "a8116ef965c1ebd103e223545794bd0a6691edd3ec678ec07972d473e2badc95"}, diff --git a/app/test/linear_cli/cli/issue_commands_test.exs b/app/test/linear_cli/cli/issue_commands_test.exs index 8dcc1d3..ff0f087 100644 --- a/app/test/linear_cli/cli/issue_commands_test.exs +++ b/app/test/linear_cli/cli/issue_commands_test.exs @@ -3029,4 +3029,127 @@ defmodule LinearCli.CLI.IssueCommandsTest do assert output =~ "--from and --to must both be given" end end + + describe "issue comment" do + defp stub_lookup_and(pairs) do + Req.Test.stub(LinearCli.Api, fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + decoded = Jason.decode!(body) + query = decoded["query"] + + cond do + String.contains?(query, "issue(id: $id)") -> + Req.Test.json(conn, %{"data" => %{"issue" => issue_map()}}) + + match = Enum.find(pairs, fn {substr, _resp} -> String.contains?(query, substr) end) -> + {_substr, resp} = match + Req.Test.json(conn, (is_function(resp, 1) && resp.(decoded)) || resp) + + true -> + raise "no stub matched query: #{query}" + end + end) + end + + test "creates a new comment" do + stub_lookup_and([{"commentCreate", comment_created()}]) + + output = + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "comment", "CRY-1", "-m", "lgtm"]) + end) + + assert output =~ "Comment added to CRY-1" + end + + test "--body-file reads the body from a file verbatim" do + path = tmp_path("body_file") + # Deliberately includes a literal backslash-n and a $VAR-looking string - + # exactly the content that broke when built as an inline shell argument + # (see documents/phase-13-plan.adoc's Goal section). + File.write!(path, "## Investigation\n\nliteral \\n and $SOME_VAR survive verbatim") + on_exit(fn -> File.rm(path) end) + + test_pid = self() + + stub_lookup_and([ + {"commentCreate", + fn decoded -> + send(test_pid, {:sent_body, decoded["variables"]["body"]}) + comment_created() + end} + ]) + + capture_io(fn -> + assert :ok = LinearCli.CLI.main(["issue", "comment", "CRY-1", "--body-file", path]) + end) + + assert_received {:sent_body, + "## Investigation\n\nliteral \\n and $SOME_VAR survive verbatim"} + end + + test "--body-file - reads the body from stdin verbatim" do + test_pid = self() + + stub_lookup_and([ + {"commentCreate", + fn decoded -> + send(test_pid, {:sent_body, decoded["variables"]["body"]}) + comment_created() + end} + ]) + + capture_io("piped from stdin\nwith a real newline", fn -> + assert :ok = LinearCli.CLI.main(["issue", "comment", "CRY-1", "--body-file", "-"]) + end) + + assert_received {:sent_body, "piped from stdin\nwith a real newline"} + end + + test "--comment and --body-file together is a smells_bad error, no GraphQL call" do + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + Req.Test.stub(LinearCli.Api, fn _conn -> raise "no GraphQL call should happen" end) + + output = + capture_io(:stderr, fn -> + LinearCli.CLI.main( + ["issue", "comment", "CRY-1", "-m", "text", "--body-file", "somefile"], + halt + ) + end) + + assert_received {:halted, 22} + assert output =~ "give --comment or --body-file, not both" + end + + test "an unreadable --body-file surfaces an error, no GraphQL call" do + test_pid = self() + halt = fn code -> send(test_pid, {:halted, code}) end + + Req.Test.stub(LinearCli.Api, fn _conn -> raise "no GraphQL call should happen" end) + + capture_io(:stderr, fn -> + LinearCli.CLI.main( + ["issue", "comment", "CRY-1", "--body-file", "/nonexistent/path/does-not-exist"], + halt + ) + end) + + assert_received {:halted, _code} + end + + test "--output json prints the resulting comment as JSON" do + stub_lookup_and([{"commentCreate", comment_created()}]) + + output = + capture_io(fn -> + assert :ok = + LinearCli.CLI.main(["issue", "comment", "CRY-1", "-m", "lgtm", "-o", "json"]) + end) + + assert %{"id" => "c1"} = Jason.decode!(output) + end + end end diff --git a/documents/phase-13-plan.adoc b/documents/phase-13-plan.adoc new file mode 100644 index 0000000..c773d7b --- /dev/null +++ b/documents/phase-13-plan.adoc @@ -0,0 +1,272 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Aug 31, 2026 +:my-title: Phase 13 plan: safe multi-line comments for Stokowski (`lc issue comment`) +:icons: font +:env-github: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:caution-caption: :fire: +:warning-caption: :warning: +endif::[] +:toc: + +[NOTE] +==== +Revision note: this phase originally shipped a find-or-update-by-marker +comment *upsert* (a single persistent "workpad" comment, appended to on +every call). That design is gone. Live verification turned up that +`commentUpdate` needs a scope this project's Linear API key doesn't +reliably carry, distinct from `commentCreate` (see Verified below) - and +separately, matching an existing comment by "body starts with a string" +is exactly the kind of fuzzy, indeterminate lookup that caused problems +in the first place (User decision, 2026-08-31). The guidance and the +command are both simpler now: post a new comment per milestone, never +update one. Everything below describes the *current*, shipped design; +the upsert history lives only in this note and in git history. +==== + +== Goal + +`AGENTS.md` says "Always only use `mix lc` to interact with linear.app. +Do not look for an mcp or any other way." That rule is right, but nothing +in `mix lc` made it easy to follow for a large, multi-line comment body - +the only option was `--comment "text"` (a single shell argument) or +`--comment -` (an interactive editor, useless headless). + +This gap is not theoretical. On EXT-17's `code-review` run 4, the review +agent (real Claude Opus, not a degraded model - see the session +transcript at +`~/.claude/projects/-home-bougybot--local-share-stokowski-workspaces-linear-cli-EXT-17/55e18e70-*.jsonl`, +lines 121 and 125) needed to post a multi-line status comment and, finding +no safe `mix lc` way to do it, hand-rolled a direct `curl`/GraphQL call +instead. That ad hoc script had two bugs of its own (a single-quoted bash +string with literal `\n` instead of real newlines, and a variable +reference inside a *quoted* heredoc delimiter that never expanded) - +exactly the class of failure "always use `mix lc`" exists to prevent. + +This phase adds a `lc issue comment` subcommand whose only real job is +`--body-file`: read the comment body from a file (or stdin), so a caller +never has to build a multi-line body as a single shell argument. It does +not touch `issue update --comment`/`issue status --comment` (plain +create, already correct and well-tested) or `bin/lcomment` (a +human-interactive wrapper, out of scope here). + +== Decisions (reached in full agreement before implementation) + +1. *A new `lc issue comment` subcommand, not a widening of `issue + update`.* Even reduced to plain create, it's a distinct, single-issue, + comment-only verb worth its own name and help text, matching Phase + 12's `issue move` reasoning for a similarly-shaped new command. +2. *Always creates a new comment. No update, no upsert, no marker + matching.* `commentUpdate` requires a scope this project's Linear API + key doesn't reliably carry (Verified below) - even a key described as + having "full access" failed it consistently, including against a + comment created moments earlier by that same key, ruling out an + ownership explanation. Independent of that, matching an existing + comment by "body starts with this string" is an indeterminate lookup: + two different sections can collide on a prefix, and a caller has no + way to know which comment will match before it happens (User + decision, 2026-08-31). Removing the mechanism removes both problems at + once. +3. *`--body-file PATH` is the actual point of this command.* `PATH` of + `-` reads stdin; anything else is read as a file. This is what kills + the incident's bug class: a multi-line body never has to survive a + single shell argument's quoting rules. `--comment`/`-m` still exists + for a short one-line body (and keeps `WhatFor.comment_for/2`'s + existing `"-"`-opens-an-editor convention); `--comment` and + `--body-file` are mutually exclusive (both given is a `smells_bad` + error). Generalizing `--body-file` to `--description`/`--reason`/etc. + is deliberately deferred - those aren't the pattern that broke, and + widening `WhatFor.ask_or_edit/3`'s shared contract for every caller is + a bigger, separate change than this phase needs. +4. *`bin/lcomment` and `issue update --comment`/`issue status --comment` + are untouched.* They're the human/quick-comment path (always creates, + `-` always opens an editor) and remain exactly as they are. +5. *No subcommand alias in this phase.* The obvious short alias (`c`) is + already `issue create`'s; picking a different one isn't worth guessing + at before the command has seen real use (same reasoning as Phase 12 + deferring a `bin/` wrapper). +6. *Guidance changes too: post one comment per milestone, never a single + running comment.* `.ai/prompts/global.md` and the per-stage prompts + (`investigate.md`, `implement.md`, `merge.md`) previously told agents + to maintain and append to a single "workpad" comment. That's gone - + each stage posts its own comment describing just that step + (investigation findings, implementation results, merge confirmation, + etc.), matching how `review.md`'s `## Code Review` already worked + (a fresh comment per run, never edited) and requiring no + `commentUpdate` capability at all. + +== Verified + +* `app/lib/linear_cli/linear/comment.ex`: `Comment.base_fields/0` + already selects everything a create response needs (`id body url + user {...}`) - no fragment change required for plain create. +* `app/lib/linear_cli/cli/issue_helpers.ex` (`issue_comment/2`, + `update_issue/2`/`maybe_comment/2`/`dispatch_update/2`): confirms + `issue update --comment "text"` with no other flag is *already* + comment-only (the `opts[:comment] -> :ok` branch in `dispatch_update/2` + fires only after `close`/`cancel`/`pr`/`project`/`description` all + miss, so no extra `issueUpdate` mutation is sent) - kept exactly as + is; `lc issue comment` doesn't call it (see Building blocks) so its + `--output json` confirmation-suppression need doesn't force a + signature change onto that shared function. +* `app/lib/linear_cli/cli/what_for.ex` (`ask_or_edit/3`, `comment_for/2`): + `thing && thing != "-"` returns `thing` unchanged - already idempotent + for a body that arrived fully resolved from `--body-file`, so routing + every body (file-sourced or not) through `comment_for/2` is safe and + keeps one resolution path. +* `app/lib/linear_cli/cli/commands.ex` (`apply_move/2` / + `print_move_results/3`): the house pattern for "call the domain + interface directly instead of the `IssueHelpers` wrapper, so the + confirmation print can be gated on `--output json`" - `issue_comment/1` + follows the same pattern against `Linear.add_comment/2` directly + rather than `IssueHelpers.issue_comment/2`. +* Incident evidence: the two malformed EXT-17 comments (session + `55e18e70-2e53-437f-ad0d-722332190839`, lines 121 and 125) are a + single-quoted `REVIEW_BODY='...\n\n...'` bash string (literal + backslash-n, not a newline) and a `<<'ENDOFBODY'` *quoted* heredoc whose + `$WORKPAD_BODY` reference therefore never expanded - both are exactly + the shell-quoting failure modes `--body-file` (write the body to a real + file, reference it by path, no inline multi-line shell argument at all) + removes. +* Live, against real Linear (`EXT-17`), with a key later primed for + write access: `mix lc issue comment EXT-17 -m "## Phase 13 + Verification\n\n..."` and a follow-up `--marker`-matched call (from the + now-abandoned design) both succeeded as plain `commentCreate` calls + (`comment-3bf46109`, `comment-7be28072`, `comment-00c123db`), each + returning a real comment URL. The follow-up `commentUpdate` call against + the very comment just created by that same key was rejected twice: + `403 FORBIDDEN: Invalid scope: 'write' required`. `mix lc whoami` + succeeds with the same key; `commentCreate` succeeds; only + `commentUpdate` doesn't - confirming the scope gap this decision is + based on, not a code defect or an ownership restriction. +* `--body-file -` piped a body containing a literal `\n` (backslash-n) + and a `$WORKPAD_BODY`-looking string plus one real newline + (`comment-ad185412`); reading it back via `--output json` and + `repr()`-ing the raw string confirmed both the literal two-character + sequence and the `$VAR`-looking text survived byte-for-byte, and the + real newline was a distinct `U+000A` - the exact incident bug class, + now provably absent. +* Test artifacts left on `EXT-17` as a result of the above (harmless, on + an already-messy `Done` issue; `lc` has no comment-delete capability to + clean them up): several comments from these verification runs. + +== Building blocks + +* `app/lib/linear_cli/cli.ex`: `comment:` entry in the `issue` subcommand + tree - `args: [issue_id: [value_name: "ISSUE_ID", help: "The Issue + (i.e. CRY-1)", required: true]]`, `options: [comment: [short: "-m", + long: "--comment", help: "Comment text. - opens an editor"], + body_file: [long: "--body-file", help: "Read the comment body from + this file (- for stdin) instead of --comment"]]`. `defp dispatch + ([:issue, :comment], result, halt), do: run(&Commands.issue_comment/1, + result, halt)` alongside the existing `:move`/`:status`/`:update` + clauses. +* `app/lib/linear_cli/cli/commands.ex`: new `issue_comment/1`: + . `--comment` and `--body-file` both given -> `{:error, {:smells_bad, + "give --comment or --body-file, not both"}}`, checked before + anything else. + . `--body-file` given: `"-"` reads `IO.read(:stdio, :eof)` (`:eof` on + empty input normalized to `""`; `IO.binread/2`, tried first, crashes + `ExUnit.CaptureIO`'s `StringIO`-backed device on this Elixir/OTP + version and is best avoided for that reason alone), anything else + `File.read/1`; a read error surfaces as `{:error, reason}` through + the normal `handle_error/3` path (no new error shape needed - + `File.read/1`'s `{:error, posix}` already round-trips fine as a + system-failure `reason`). + . resolve the single issue via `Linear.issues(%{ids: + [IssueHelpers.expand_issue_id(issue_id)]})` (same call `issue_update`/ + `issue_status` already make), erroring the same way on zero results. + . `body = WhatFor.comment_for(issue, comment_text)`, then + `Linear.add_comment(issue.identifier, body)` directly (not + `IssueHelpers.issue_comment/2` - see Verified above); prints + `Prompt.ok("Comment added to #{issue.identifier}")` unless + `options.output == "json"`, then `Display.show(comment, %{output: + options.output})`. +* `app/lib/linear_cli/cli/display.ex`: found during implementation, not + anticipated above - `puts_text/2` (text-mode rendering) had no + `%Comment{}` clause at all (`Comment` was previously only ever shown + nested inside `issue ls --full`'s own rendering, never standalone), so + calling `Display.show` on a bare comment raised `FunctionClauseError`, + which `LinearCli.CLI.main/2`'s top-level `rescue` turns into a real + `System.halt/1` call - not a test-only problem, a genuine crash any + real invocation of this command would have hit. New clause: `IO.puts + (comment.url || "(no URL returned)")`, mirroring `ProjectUpdate`'s own + "link back to what was just posted" convention. JSON mode was already + fine (`to_plain/1`'s generic `%_struct{}` clause covers any resource). +* No changes to `app/lib/linear_cli/linear/comment.ex`, + `app/lib/linear_cli/linear.ex`, or `documents/ash-domain-erd.adoc` - + `add_comment`/`commentCreate` is the only mutation this command needs, + and it already existed before this phase. + +== Tests + +All in `app/test/linear_cli/cli/issue_commands_test.exs` (full-command +layer - not a new `issue_comment_test.exs` file; every other +`Commands.issue_*` function's tests, including `issue_move`'s, live in +that one consolidated file, and `issue_comment` follows the same +convention), on the existing `Req.Test.stub(LinearCli.Api, ...)` harness. +New `describe "issue comment"`: + +* Plain create: sends `commentCreate`; stdout shows "Comment added to + ...". +* `--body-file `: file contents (including embedded literal + backslash-n or `$VAR`-looking text, proving no shell ever touches the + content) become the comment body verbatim. +* `--body-file -`: stdin contents become the body verbatim. +* `--comment` and `--body-file` together: no GraphQL call is sent at all; + `{:error, {:smells_bad, _}}` surfaces through `handle_error/3`'s exit + path. +* Unreadable `--body-file` path: `{:error, reason}` surfaces the same + way, no GraphQL call is sent. +* `--output json`: JSON is the comment map, with no `Prompt.ok` + confirmation text mixed into stdout. + +== Docs + +* `AGENTS.md`: "The Plan" index gains the Phase 12 and Phase 13 entries + (Phase 12's was missing too); "Rules for Interacting with Linear" gains + `mix lc issue comment --help` alongside the existing example commands. +* `Readme.adoc`: `issue` command table gains a `comment` row (no alias, + per Decision 5); a short example block under the existing `issue + update`/`issue status` examples showing plain create and + `--body-file`. +* `.ai/prompts/global.md`: the old "Linear workpad" section (a single + persistent comment, found and appended to by marker) is replaced with + "Linear progress updates" - post a new comment per milestone via `mix + lc issue comment --body-file `, each one covering only that + step's findings/decisions/results; read prior comments for context but + never try to edit one. The "Rework awareness" list's "Prior workpad + content (append to it, do not overwrite)" bullet becomes "Prior + progress comments (read them for context; post a new comment for this + run rather than editing an old one)". +* `.ai/prompts/investigate.md`, `implement.md`, `merge.md`: every + "update/append to the workpad" instruction becomes "post a Linear + comment" - `investigate.md`'s rework step now says to post a new `## + Investigation (rework)` comment instead of "updating" the prior one. + `review.md` needed no change - its rework step already said "Post an + updated `## Code Review` comment" (a new comment, not an edit), which + is the pattern every stage now follows. + +== Sequencing + +No dependency on unlanded work. + +1. Optimus `issue comment` spec, `dispatch/3` clause, and + `Commands.issue_comment/1` (body-file/mutual-exclusion handling + first, then plain create) + the full CLI test file. +2. `AGENTS.md`, `Readme.adoc`, `.ai/prompts/global.md`, + `.ai/prompts/investigate.md`, `.ai/prompts/implement.md`, + `.ai/prompts/merge.md`. +3. Manual end-to-end verification against live Linear (dogfooding, per + `app/usage-rules.md`) - done; see Verified above: plain create and + `--body-file` (file and stdin) both confirmed against real Linear, + including the literal-backslash-n/`$VAR`-survives-verbatim proof that + directly closes the incident's bug class. + +Standard workflow from here: file a GitHub issue for this phase, branch +from it, commit, open the PR - no direct-to-main commits. Conventional +commit type: `feat`. diff --git a/mise.toml b/mise.toml index 079fd95..5e5a88a 100644 --- a/mise.toml +++ b/mise.toml @@ -7,3 +7,13 @@ erlang = "29.0.3" elixir = "1.20.3" python = "3.14.1" +[env] +#ANTHROPIC_BASE_URL = "http://127.0.0.1:13305" +#ANTHROPIC_AUTH_TOKEN = "lemonade" +#LEMONADE_API_KEY = "lemonade" +#ANTHROPIC_DEFAULT_OPUS_MODEL = "user.Qwen3.6-35B-A3B-ThinkingCoder" +#ANTHROPIC_DEFAULT_SONNET_MODEL = "user.Qwen3.6-35B-A3B-ThinkingCoder" +#ANTHROPIC_DEFAULT_HAIKU_MODEL = "user.Qwen3.6-35B-A3B-ThinkingCoder" +#CLAUDE_CODE_SUBAGENT_MODEL = "user.Qwen3.6-35B-A3B-ThinkingCoder" +#CLAUDE_CODE_ATTRIBUTION_HEADER = 0 +#CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1 diff --git a/vendor/stokowski b/vendor/stokowski index 6042552..73bbbcf 160000 --- a/vendor/stokowski +++ b/vendor/stokowski @@ -1 +1 @@ -Subproject commit 60425520c26ca2c1b53a4e408bdebcc4d1786b84 +Subproject commit 73bbbcf6e4284e768263df23c673948fe85eb81c diff --git a/workflow.claude.yaml b/workflow.claude.yaml new file mode 100644 index 0000000..5f70a42 --- /dev/null +++ b/workflow.claude.yaml @@ -0,0 +1,263 @@ +# ============================================================================= +# Stokowski — Example State Machine Workflow +# ============================================================================= +# +# 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 .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 +# 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 + assignee: me # only issues assigned to the API key's user + +# --------------------------------------------------------------------------- +# 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: 60000 # 15 seconds between ticks + +# --------------------------------------------------------------------------- +# Workspace +# --------------------------------------------------------------------------- +workspace: + root: ~/.local/share/stokowski/workspaces/linear-cli + +# --------------------------------------------------------------------------- +# 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: | + set -eu + git fetch origin main + changes="$(git status --porcelain)" + if [ -n "$changes" ]; then + echo "before_run: preserving uncommitted workspace changes; skipping rebase" + else + git rebase origin/main + fi + 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 + max_turns: 20 + turn_timeout_ms: 3600000 # 1 hour per turn + stall_timeout_ms: 900000 # 15 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: .ai/prompts/investigate.md + linear_state: active + 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: .ai/prompts/implement.md + linear_state: active + 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: .ai/prompts/review.md + linear_state: active + runner: claude # "claude" (default) or "codex" + 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: .ai/prompts/merge.md + linear_state: active + max_turns: 5 + session: inherit + transitions: + complete: done + + # ── 8. Done (terminal) ───────────────────────────────────────────────── + done: + type: terminal + linear_state: terminal diff --git a/workflow.codex.yaml b/workflow.codex.yaml new file mode 100644 index 0000000..e5f8ffb --- /dev/null +++ b/workflow.codex.yaml @@ -0,0 +1,267 @@ +# ============================================================================= +# Stokowski — Example State Machine Workflow +# ============================================================================= +# +# 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 .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 +# 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 Codex 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`, legacy `claude` runner settings, +# `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: { turn_timeout_ms: 3600000 } # shared runner settings +# 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 + assignee: me # only issues assigned to the API key's user + +# --------------------------------------------------------------------------- +# 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: 60000 # 15 seconds between ticks + +# --------------------------------------------------------------------------- +# Workspace +# --------------------------------------------------------------------------- +workspace: + root: ~/.local/share/stokowski/workspaces/linear-cli + +# --------------------------------------------------------------------------- +# 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: | + set -eu + git fetch origin main + changes="$(git status --porcelain)" + if [ -n "$changes" ]; then + echo "before_run: preserving uncommitted workspace changes; skipping rebase" + else + git rebase origin/main + fi + timeout_ms: 120000 + +# --------------------------------------------------------------------------- +# Shared runner defaults +# +# Stokowski 0.5 keeps these runner-neutral limits under its legacy `claude` +# config key. Leave `model` unset so Codex inherits the invoking user's model; +# each agent state sets an explicit reasoning effort so an interactive `max` +# default cannot make unattended turns run indefinitely. +# --------------------------------------------------------------------------- +claude: + max_turns: 20 + turn_timeout_ms: 3600000 # 1 hour per turn + stall_timeout_ms: 900000 # 15 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 the invoking user's configured Codex model with bounded reasoning. + investigate: + type: agent + prompt: .ai/prompts/investigate.md + linear_state: active + runner: codex + reasoning_effort: medium + max_turns: 8 + 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. + implement: + type: agent + prompt: .ai/prompts/implement.md + linear_state: active + runner: codex + reasoning_effort: xhigh + max_turns: 30 + 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. + # Codex reviews the diff and posts findings. + code-review: + type: agent + prompt: .ai/prompts/review.md + linear_state: active + runner: codex + reasoning_effort: high + max_turns: 10 + 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: .ai/prompts/merge.md + linear_state: active + runner: codex + reasoning_effort: medium + max_turns: 5 + transitions: + complete: done + + # ── 8. Done (terminal) ───────────────────────────────────────────────── + done: + type: terminal + linear_state: terminal diff --git a/workflow.opus.yaml b/workflow.opus.yaml new file mode 100644 index 0000000..dcefed7 --- /dev/null +++ b/workflow.opus.yaml @@ -0,0 +1,267 @@ +# ============================================================================= +# Stokowski — Example State Machine Workflow +# ============================================================================= +# +# 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 .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 +# 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 + assignee: me # only issues assigned to the API key's user + +# --------------------------------------------------------------------------- +# 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: 60000 # 15 seconds between ticks + +# --------------------------------------------------------------------------- +# Workspace +# --------------------------------------------------------------------------- +workspace: + root: ~/.local/share/stokowski/workspaces/linear-cli + +# --------------------------------------------------------------------------- +# 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: | + set -eu + git fetch origin main + changes="$(git status --porcelain)" + if [ -n "$changes" ]; then + echo "before_run: preserving uncommitted workspace changes; skipping rebase" + else + git rebase origin/main + fi + 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: .ai/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: .ai/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: .ai/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: .ai/prompts/merge.md + linear_state: active + max_turns: 5 + session: inherit + transitions: + complete: done + + # ── 8. Done (terminal) ───────────────────────────────────────────────── + done: + type: terminal + linear_state: terminal diff --git a/workflow.yaml b/workflow.yaml deleted file mode 100644 index dcefed7..0000000 --- a/workflow.yaml +++ /dev/null @@ -1,267 +0,0 @@ -# ============================================================================= -# Stokowski — Example State Machine Workflow -# ============================================================================= -# -# 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 .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 -# 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 - assignee: me # only issues assigned to the API key's user - -# --------------------------------------------------------------------------- -# 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: 60000 # 15 seconds between ticks - -# --------------------------------------------------------------------------- -# Workspace -# --------------------------------------------------------------------------- -workspace: - root: ~/.local/share/stokowski/workspaces/linear-cli - -# --------------------------------------------------------------------------- -# 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: | - set -eu - git fetch origin main - changes="$(git status --porcelain)" - if [ -n "$changes" ]; then - echo "before_run: preserving uncommitted workspace changes; skipping rebase" - else - git rebase origin/main - fi - 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: .ai/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: .ai/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: .ai/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: .ai/prompts/merge.md - linear_state: active - max_turns: 5 - session: inherit - transitions: - complete: done - - # ── 8. Done (terminal) ───────────────────────────────────────────────── - done: - type: terminal - linear_state: terminal diff --git a/workflow.yaml b/workflow.yaml new file mode 120000 index 0000000..9791530 --- /dev/null +++ b/workflow.yaml @@ -0,0 +1 @@ +workflow.opus.yaml \ No newline at end of file