diff --git a/.github/workflows/linear-project.yml b/.github/workflows/linear-project.yml new file mode 100644 index 00000000..13004c7e --- /dev/null +++ b/.github/workflows/linear-project.yml @@ -0,0 +1,45 @@ +# Puts each new GitHub issue's Linear copy into the Linear project cloudpid (scripts/linear-project.sh). Linear's GitHub +# Issues sync copies the issue into the team SPI but into no project, and agents read issues by project +# (docs/agents/issue-tracker.md). It runs for `cloudpid-report` issues too: the GitHub App opens them, and an issue +# opened by an App starts workflows (one opened with GITHUB_TOKEN would not). A dispatch takes an issue number, to retry +# one whose run failed; an issue opened before the sync existed has no copy to move. Needs the repository secret +# LINEAR_API_KEY: a Linear API key that may edit SPI's issues. +name: linear project + +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue: + description: GitHub issue number + required: true + type: number + +permissions: {} + +concurrency: + group: linear-project-${{ github.event.issue.number || inputs.issue }} + +jobs: + project: + name: project + # Forks have neither the secret nor the Linear project. + if: github.repository == 'ian-pascoe/cloudpid' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read # sparse checkout of the script + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + sparse-checkout: scripts/linear-project.sh + sparse-checkout-cone-mode: false + - name: Add to the project + env: + LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }} + # The Linear project cloudpid (team SPI). + LINEAR_PROJECT: c5b4ec54-51b3-4742-88a1-0a75c7c646a8 + ISSUE_URL: ${{ github.event.issue.html_url || format('{0}/{1}/issues/{2}', github.server_url, github.repository, inputs.issue) }} + run: scripts/linear-project.sh diff --git a/AGENTS.md b/AGENTS.md index e81c014a..eb2c6856 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ Every workspace package (`apps/*`, `infra`, `packages/*`) is named `@cloudpid/`. + +Issues labelled `cloudpid-report` were filed by a cloudpid agent through the GitHub App about cloudpid itself (the `cloudpid_report` tool, ADR-0051); their footer, after the last `---`, names the stage, version, model and Session they came from. Triage their Linear copies like any `needs-triage` issue (`docs/agents/triage-labels.md`). -- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies. -- **Read an issue**: `gh issue view --json number,title,body,labels,comments`. -- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters. -- **Make an issue a sub-issue of a parent**: `gh issue create --parent ...`, or `gh issue edit --add-sub-issue ` afterwards (`gh` 2.94+). Older `gh`: `gh api --method POST repos///issues//sub_issues -F sub_issue_id=` (database id, as in **Blocking** below). Without sub-issues, put `Part of #` at the top of the child body. -- **Comment on an issue**: `gh issue comment --body "..."` -- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."` -- **Close**: `gh issue close --comment "..."` +## How to reach Linear -Issues labelled `cloudpid-report` were filed by a cloudpid agent through the GitHub App about cloudpid itself (the `cloudpid_report` tool, ADR-0051); their footer, after the last `---`, names the stage, version, model and Session they came from. Triage them like any `needs-triage` issue (`docs/agents/triage-labels.md`). +1. **Executor (default).** Executor's Linear app, through its MCP tools `call` / `execute`, under `tools.linear.*` with account `acc_0c3d8286-c8a7-491e-ae0c-e433e1367c79`. + - Every tool takes `{ accountId, input: { arguments: { … }, select: "" } }`. + - Issue ids accept identifiers (`SPI-123`). + - A call may pause for the user's approval. Show the request and resume it; never repeat the call. +2. **The `linear` CLI (fallback)**, `@schpet/linear-cli`, installed globally through mise and signed in with `linear auth login`. Use it when: + - Executor's tools aren't available in the session; + - its Linear app fails; + - the user declines an approval. -Infer the repo from `git remote -v`; `gh` does this automatically when run inside a clone. + With the CLI: + - Pass `--team SPI` to every query (no repo default is configured). + - Pass `--no-input` to writes. + - Pass markdown through `--description-file` / `--body-file`. + - Use `linear api` (GraphQL on stdin) for anything without a dedicated command. -## Pull requests as a triage surface +Both reach the same API, so the GraphQL selections below work in either. -**PRs as a request surface: no.** _(Set to `yes` if this repo treats external PRs as feature requests; `/triage` reads this flag.)_ +| Thing | Value | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Team `SPI` | `86ad747a-262a-4ab8-a716-3a7b3cd5a6af` | +| Project `cloudpid` | `c5b4ec54-51b3-4742-88a1-0a75c7c646a8` | +| States (name: type) | Backlog: backlog `0b2345bf-8a16-4fd5-8150-ab08e0d74a26` · Todo: unstarted `ad8f9e8b-f89d-4623-8571-68607c315057` · Blocked: unstarted `e417759b-5003-44fd-bfdd-fc6159afb145` · In Progress: started `a52ed858-c23e-4098-8fd1-a313fb0f2204` · In Review: started `62d08185-be69-4fe5-873a-1710a52fb9aa` · Done: completed `d8e326e4-cff3-4cca-a760-5b19efccf264` · Canceled: canceled `9bff72eb-c2dc-4f1a-a6a5-c7522d98fc15` | -When set to `yes`, PRs run through the same labels and states as issues, using the `gh pr` equivalents: +Label ids are in `docs/agents/triage-labels.md`. For any other label: `tools.linear.query_issueLabels` with `filter: { name: { eq: "" }, team: { key: { eq: "SPI" } } }`, or `linear label list --team SPI --json`. -- **Read a PR**: `gh pr view --comments` and `gh pr diff ` for the diff. -- **List external PRs for triage**: `gh api --paginate 'repos/{owner}/{repo}/pulls?state=open' --jq '.[] | select(.author_association | IN("OWNER","MEMBER","COLLABORATOR") | not) | {number, title, author: .user.login, author_association, labels: [.labels[].name]}'`. -- **Comment / label / close**: `gh pr comment`, `gh pr edit --add-label`/`--remove-label`, `gh pr close`. +## Conventions -GitHub shares one number space across issues and PRs, so a bare `#42` may be either: resolve with `gh pr view 42` and fall back to `gh issue view 42`. +- **Create an issue**: + - Executor: `tools.linear.mutation_issueCreate`, `arguments: { input: { teamId, projectId, title, description, labelIds?, parentId? } }`, `select: "identifier url"`. + - CLI: `linear issue create --team SPI --project cloudpid --title "…" --description-file body.md [--label ] [--parent SPI-1] --no-input`. +- **Read an issue**: + - Executor: `tools.linear.query_issue`, `arguments: { id: "SPI-123" }`, `select: "identifier title description url state { name type } labels { nodes { name } } assignee { name } parent { identifier } children { nodes { identifier title state { type } } } comments { nodes { body createdAt user { name } } } attachments { nodes { url } }"`. + - CLI: `linear issue view SPI-123 --json`. + - From a GitHub number: Executor `tools.linear.query_attachmentsForURL`, `arguments: { url: "https://github.com/ian-pascoe/cloudpid/issues/" }`, `select: "nodes { sourceType issue { identifier } }"`; take the node whose `sourceType` is `github` (the sync's; another issue may link the same URL by hand). The CLI uses the same query through `linear api`. +- **List issues**: + - Executor: `tools.linear.query_issues`, `arguments: { first: 100, filter: { project: { id: { eq: "" } }, state: { type: { nin: ["completed", "canceled"] } }, labels: { name: { eq: "