Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions .ai/prompts/global.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ISSUE_ID> --body-file <path>

- 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 <ISSUE_ID>`); 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

Expand All @@ -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)
11 changes: 8 additions & 3 deletions .ai/prompts/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,7 +41,7 @@ Implement the solution, create a PR, and ensure it passes all quality checks.
gh pr create --title "{{ issue.identifier }}: <concise title>" --body "<description>"
```
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

Expand All @@ -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

Expand All @@ -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
5 changes: 2 additions & 3 deletions .ai/prompts/investigate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
8 changes: 5 additions & 3 deletions .ai/prompts/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ mechanical stage — no new code changes.
```
gh pr merge <number> --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

Expand All @@ -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
1 change: 1 addition & 0 deletions .ai/prompts/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 17 additions & 0 deletions Readme.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions app/lib/linear_cli/cli.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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...)",
Expand Down
49 changes: 49 additions & 0 deletions app/lib/linear_cli/cli/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 10 additions & 1 deletion app/lib/linear_cli/cli/display.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: " "

Expand Down
4 changes: 2 additions & 2 deletions app/mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
Expand Down Expand Up @@ -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"},
Expand Down
Loading
Loading