From 07b89123c333f68a3ecf83d61dec259976a77e0e Mon Sep 17 00:00:00 2001 From: "Tj (bougyman) Vanderpoel" Date: Tue, 22 Sep 2026 15:39:34 -0400 Subject: [PATCH] docs(style): adds plain english guidelines and document formatting skill --- .agents/skills | 1 + .ai/skills/documents/SKILL.md | 113 ++++ .../documents/assets/decisions/decision.adoc | 38 ++ .../assets/decisions/related-decision.adoc | 32 ++ .../documents/assets/document-template.adoc | 16 + .../assets/example-ash-domain-erd.adoc | 540 ++++++++++++++++++ .../example-decision-burrito-decision.adoc | 72 +++ .ai/skills/plain-english | 1 + .ai/skills/simple-english | 1 + .claude/skills | 1 + .gitmodules | 3 + AGENTS.md | 16 +- vendor/claude-plain-english-skill | 1 + 13 files changed, 834 insertions(+), 1 deletion(-) create mode 120000 .agents/skills create mode 100644 .ai/skills/documents/SKILL.md create mode 100644 .ai/skills/documents/assets/decisions/decision.adoc create mode 100644 .ai/skills/documents/assets/decisions/related-decision.adoc create mode 100644 .ai/skills/documents/assets/document-template.adoc create mode 100644 .ai/skills/documents/assets/example-ash-domain-erd.adoc create mode 100644 .ai/skills/documents/assets/example-decision-burrito-decision.adoc create mode 120000 .ai/skills/plain-english create mode 120000 .ai/skills/simple-english create mode 120000 .claude/skills create mode 160000 vendor/claude-plain-english-skill diff --git a/.agents/skills b/.agents/skills new file mode 120000 index 0000000..6838a11 --- /dev/null +++ b/.agents/skills @@ -0,0 +1 @@ +../.ai/skills \ No newline at end of file diff --git a/.ai/skills/documents/SKILL.md b/.ai/skills/documents/SKILL.md new file mode 100644 index 0000000..6af69b8 --- /dev/null +++ b/.ai/skills/documents/SKILL.md @@ -0,0 +1,113 @@ +--- +name: documents +description: >- + Create or edit Fantasia repository documentation. Use for plans, decisions, + architecture notes, runbooks, README files, and other project prose. Write + AsciiDoc with the repository header, lowercase kebab-case filenames, native + AsciiDoc markup, clean link attributes, and Mermaid diagrams. +--- + +# Fantasia documentation + +Write repository documentation in AsciiDoc. Use the `.adoc` extension. + +Use this skill when you create or edit project prose. This includes plans, +decisions, architecture notes, runbooks, and README files. Do not convert an +existing document unless the user asks for the conversion. + +## File names + +Use lowercase kebab-case file names. Use words that describe the document. + +* Write `repository-management-decision.adoc`. +* Do not write `REPOSITORY_MANAGEMENT_DECISION.adoc`. +* Keep a required numeric prefix when an existing document series uses one, + such as `0009-new-decision.adoc`. + +## Document locations + +Store authored documents under `documents/` at the repository root. Put each +document in the closest matching subdirectory. + +* Store decisions in `documents/decisions/`. +* Store plans and implementation notes in `documents/plans/`. +* Store agent learning entries in `documents/agent_learnings/`. +* Keep only document indexes at `documents/` itself. + +Do not add project documentation beside source files or at the repository root. +If no existing subdirectory fits, create a lowercase kebab-case subdirectory +under `documents/`. + +## Start from the template + +Copy `assets/document-template.adoc` when you create a document. Set +`my-title` and `revdate` before you write the body. Keep the author line and +the GitHub icon block. + +Use the date format in the template: `Mon DD, YYYY`. For example, +`Sep 21, 2026`. + +Add `:toc:` when the document has several sections or readers need to scan it. +Do not add a table of contents to a short note unless it helps navigation. + +## Write native AsciiDoc + +Use AsciiDoc markup. Do not use Markdown syntax inside `.adoc` files. + +* Use `==` and `===` for sections. +* Use `*text*` for emphasis and backticks for code. +* Use AsciiDoc lists, tables, source blocks, admonitions, and cross references. +* Use `link:` for external links and `xref:` for document links. + +Declare each reusable URL or document target as a document attribute near the +header. Use that attribute in the body. This keeps URLs and paths out of prose. + +```adoc +:repository-management-decision: ../decisions/repository-management-mix-tasks-decision.adoc[the repository-management Mix task ownership decision] +:managed-toolchain-refresh-decision: ../decisions/0008-managed-toolchain-refresh.adoc[the managed toolchain refresh decision] + +See xref:{repository-management-decision}. Also see +xref:{managed-toolchain-refresh-decision}. +``` + +The paths above apply to a document in `documents/plans/`. Make document paths +relative to the document that contains them. + +Do not create an attribute for a link that appears once and reads better as a +normal cross reference. Keep attribute names short, specific, and lowercase. + +## Diagrams + +Use Mermaid for diagrams. Put Mermaid source in an AsciiDoc source block. +Describe the diagram in nearby prose or provide an equivalent table when the +diagram carries important information. + +```adoc +[source,mermaid] +---- +flowchart LR + Source --> Build --> Release +---- +``` + +## Before you finish + +Make sure that every created or materially edited document has the template +header. Check these items: + +* The file uses `.adoc` and a lowercase kebab-case name. +* The header includes the title, author, `revdate`, `:icons: font`, + `:env-github:`, and the GitHub caption block. +* The body uses AsciiDoc syntax, not Markdown syntax. +* Reused links are document attributes. +* Each diagram uses Mermaid in a `[source,mermaid]` block. +* The document describes the current state. Put history in a decision record, + implementation note, or changelog when it matters. + +## References + +* `assets/document-template.adoc` is the required header template. +* `assets/example-ash-domain-erd.adoc` shows a Mermaid diagram and accessible + text alternatives. +* `assets/decisions/decision.adoc` records document location and format. +* `assets/decisions/related-decision.adoc` records link and diagram rules. diff --git a/.ai/skills/documents/assets/decisions/decision.adoc b/.ai/skills/documents/assets/decisions/decision.adoc new file mode 100644 index 0000000..ea13edd --- /dev/null +++ b/.ai/skills/documents/assets/decisions/decision.adoc @@ -0,0 +1,38 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Sep 21, 2026 +:my-title: Documentation location and format +: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: +:status: Accepted + +== Decision + +Store Fantasia project documents in `documents/` at the repository root. +Use AsciiDoc and the `.adoc` extension for authored prose. + +Store decisions in `documents/decisions/`. Store plans and implementation +notes in `documents/plans/`. Store agent learning entries in +`documents/agent_learnings/`. Keep only document indexes at `documents/`. + +Use lowercase kebab-case names. Preserve a numeric prefix when a document +series requires one. + +Every new document has the standard title, author, revision date, Font Awesome +icons, and GitHub icon captions. + +== Rationale + +One document root gives readers a predictable place to start. Subdirectories +group documents by purpose without mixing project prose with source files. + +AsciiDoc supports the repository's tables, admonitions, source blocks, and +GitHub rendering without a separate Markdown convention. diff --git a/.ai/skills/documents/assets/decisions/related-decision.adoc b/.ai/skills/documents/assets/decisions/related-decision.adoc new file mode 100644 index 0000000..0dbd738 --- /dev/null +++ b/.ai/skills/documents/assets/decisions/related-decision.adoc @@ -0,0 +1,32 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Sep 21, 2026 +:my-title: Document links and diagrams +: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: + +:document-location-decision: ./decision.adoc[the documentation location and format decision] +:status: Accepted + +== Decision + +Use document attributes for repeated URLs and cross references. Keep the link +target and its text in the attribute. Use the attribute in the document body. + +Use Mermaid in `[source,mermaid]` blocks for diagrams. Add prose or a table +when a diagram contains information that readers need without the diagram. + +== Rationale + +See xref:{document-location-decision}. + +Attributes keep long URLs and document paths out of body prose. Mermaid source +stays readable in Git and renders on GitHub. diff --git a/.ai/skills/documents/assets/document-template.adoc b/.ai/skills/documents/assets/document-template.adoc new file mode 100644 index 0000000..de2477e --- /dev/null +++ b/.ai/skills/documents/assets/document-template.adoc @@ -0,0 +1,16 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Sep 21, 2026 +:my-title: Document title +: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: + +// Add reusable links and document targets here. Keep paths relative to this document. diff --git a/.ai/skills/documents/assets/example-ash-domain-erd.adoc b/.ai/skills/documents/assets/example-ash-domain-erd.adoc new file mode 100644 index 0000000..94a318c --- /dev/null +++ b/.ai/skills/documents/assets/example-ash-domain-erd.adoc @@ -0,0 +1,540 @@ += {my-title} +Tj Vanderpoel (bougyman) +:revdate: Aug 18, 2026 +:my-title: Ash domain ERD: LinearCli.Linear +: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: + +== Architecture overview + +`LinearCli.Linear` is the single Ash domain in this application. +It has *no local data layer* — the Linear GraphQL API is the backing store. +Each resource's actions are implemented as `Ash.Resource.ManualRead`, +`Ash.Resource.ManualCreate`, or `Ash.Resource.ManualUpdate` modules that +call `LinearCli.Api.call/2` directly with hand-built GraphQL documents. + +There is no `Ecto.Repo`, no database, and no Ash migration. +Data returned from the API is decoded by each resource's `from_map/1` +function and stored in struct fields typed as `:term` or +`{:array, :term}` when they hold other resource structs. + +*No Ash DSL `relationships do` blocks exist anywhere in this domain.* +All associations described in the ERD below are GraphQL/nested-data +associations: they are populated from nested JSON in the API response, +not from Ash relationship declarations or database join logic. + +== ERD diagram + +All eight resources and their GraphQL/nested-data associations. +Every edge is labeled *[nested]* to make explicit that these are not +declared Ash relationships. Resource attributes that hold nested structs +are shown with the target resource name as their type. + +[source,mermaid] +---- +erDiagram + User { + string id PK + string name + string email + Team[] teams + } + Team { + string id PK + string key + string name + string description + } + Project { + string id PK + string name + string content + string slug_id + string description + string url + Team[] teams + } + Issue { + string id PK + string identifier + string title + string branch_name + string description + User assignee + WorkflowState state + Team team + Comment[] comments + Label[] labels + IssueRelation[] relations + IssueRelation[] inverse_relations + } + IssueRelation { + string id PK + atom direction + string type + term issue + term related_issue + } + Label { + string id PK + string name + string description + boolean is_group + } + WorkflowState { + string id PK + string name + float position + string type + string description + } + Comment { + string id PK + string body + string url + User user + } + ProjectUpdate { + string id PK + string body + string health + string url + } + + Issue }o--o| User : "assignee [nested]" + Issue }o--|| WorkflowState : "state [nested]" + Issue }o--|| Team : "team [nested]" + Issue ||--o{ Comment : "comments [nested]" + Issue ||--o{ Label : "labels [nested]" + Issue ||--o{ IssueRelation : "relations [nested, outbound]" + Issue ||--o{ IssueRelation : "inverse_relations [nested, inbound]" + User }o--o{ Team : "teams [nested]" + Comment }o--|| User : "user/author [nested]" + Project }o--o{ Team : "teams [nested]" +---- + +NOTE: All edges are GraphQL/nested-data associations. No Ash DSL +`relationships do` block exists in this domain. `Label`, +`WorkflowState`, and `ProjectUpdate` have no stored association +attributes — their team or project context is passed as action +arguments only. + +== Resources (text reference) + +The table below is the plain-text equivalent of the entity boxes in the +diagram above, included for accessibility. + +Nine resources are registered in `LinearCli.Linear` +(`app/lib/linear_cli/linear.ex`). + +[cols="1,2,3", options="header"] +|=== +| Resource module | Primary key | Public attributes + +| `LinearCli.Linear.User` +| `id` (`:string`) +| `name`, `email`, `teams` (`{:array, :term}`) + +| `LinearCli.Linear.Team` +| `id` (`:string`) +| `key`, `name`, `description` + +| `LinearCli.Linear.Project` +| `id` (`:string`) +| `name`, `content`, `slug_id`, `description`, `url`, `teams` (`{:array, :term}`) + +| `LinearCli.Linear.Issue` +| `id` (`:string`) +| `identifier`, `title`, `branch_name`, `description`, `assignee` (`:term`), `state` (`:term`), `team` (`:term`), `comments` (`{:array, :term}`), `labels` (`{:array, :term}`), `relations` (`{:array, :term}`), `inverse_relations` (`{:array, :term}`) + +| `LinearCli.Linear.Label` +| `id` (`:string`) +| `name`, `description`, `is_group` + +| `LinearCli.Linear.WorkflowState` +| `id` (`:string`) +| `name`, `position`, `type`, `description` + +| `LinearCli.Linear.Comment` +| `id` (`:string`) +| `body`, `url`, `user` (`:term`) + +| `LinearCli.Linear.ProjectUpdate` +| `id` (`:string`) +| `body`, `health`, `url` + +| `LinearCli.Linear.IssueRelation` +| `id` (`:string`) +| `type` (`:string`), `direction` (`:atom`, `:outbound` or `:inbound`), `issue` (`:term`), `related_issue` (`:term`) +|=== + +== Associations (text reference) + +The table below is the plain-text equivalent of the diagram edges above, +included for accessibility. + +All associations in this domain are populated from nested GraphQL response +data, not from declared Ash relationships. +*No Ash DSL relationship* is declared anywhere in the domain. + +[cols="1,1,1,2,2", options="header"] +|=== +| Source | Target | Cardinality | Attribute on source | Implementation type + +| `Issue` +| `User` +| many-to-one (assignee) +| `assignee` (`:term`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.assignee` + +| `Issue` +| `WorkflowState` +| many-to-one (current state) +| `state` (`:term`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.state` + +| `Issue` +| `Team` +| many-to-one +| `team` (`:term`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.team` + +| `Issue` +| `Comment` +| one-to-many +| `comments` (`{:array, :term}`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.comments.nodes` (full fragment only) + +| `Issue` +| `Label` +| one-to-many +| `labels` (`{:array, :term}`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.labels.nodes` (full fragment only) + +| `Issue` +| `IssueRelation` +| one-to-many (outbound) +| `relations` (`{:array, :term}`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.relations.edges` (full fragment only); direction set to `:outbound` + +| `Issue` +| `IssueRelation` +| one-to-many (inbound) +| `inverse_relations` (`{:array, :term}`) +| GraphQL/nested-data — populated by `Issue.from_map/1` from `issue.inverseRelations.edges` (full fragment only); direction set to `:inbound` + +| `User` +| `Team` +| many-to-many +| `teams` (`{:array, :term}`) +| GraphQL/nested-data — populated by `User.from_map/1` from `viewer.teams.nodes` + +| `Comment` +| `User` +| many-to-one (author) +| `user` (`:term`) +| GraphQL/nested-data — populated by `Comment.from_map/1` from `comment.user` + +| `Project` +| `Team` +| many-to-many +| `teams` (`{:array, :term}`) +| GraphQL/nested-data — populated by `Project.from_map/1` from `project.teams.nodes` (via `fields_with_teams/0`) + +| `Team` +| `Project` +| one-to-many (implicit) +| _(none — not stored on `Team`)_ +| GraphQL/nested-data — fetched via `Team.full_fields/0` in `Team.Read.Find`; the result is not stored as a `Team` attribute + +|=== + +=== Notes on unidirectional associations + +* `Label` is stored as an association attribute on `Issue` (`labels`, `{:array, :term}`) — + populated from `issue.labels.nodes` in `full_fields` responses and in compact list + responses when `include_labels: true` or `labels` is non-empty (via `list_fields_with_labels/0`). + However, `Label` itself has no stored back-reference to its team; the + `team_id` is passed as an action argument to `Label.Read.ByTeam` only. +* `WorkflowState` has no stored association attributes. + Its `team_id` appears only as a query argument passed to its read + actions; there is no `:team` field on that struct. +* `ProjectUpdate` has no stored association attributes. + Its `project_id` appears only as a required create argument. +* When `Team.Read.Find` fetches a team by id, the response includes + `projects { nodes { ... } }` (via `Team.full_fields/0`), but those + projects are not stored on the `Team` struct — the list is available + to the caller inline and is not persisted anywhere. + +== Action and code-interface matrix + +The table below maps every domain code-interface (defined in +`app/lib/linear_cli/linear.ex`) to its backing action, action type, +manual-implementation module, and the Linear GraphQL operation it calls. + +[cols="1,1,1,2,2,2", options="header"] +|=== +| Resource | Code interface | Action name | Action type | Manual module | GraphQL operation + +| `User` +| `me` +| `:me` +| read (`get?: true`) +| `Linear.User.Read.Me` +| `{ viewer { ... } }` — `viewer` query with `User.fields_with_teams/0` + +| `Team` +| `teams` +| `:all` +| read +| `Linear.Team.Read.All` +| `teams(first:, after:)` — paginated with `Team.base_fields/0` + +| `Team` +| `my_teams` +| `:mine` +| read +| `Linear.Team.Read.Mine` +| delegates to `User.Read.Me`, returns `user.teams` + +| `Team` +| `find_team` +| `:find` +| read (`get?: true`) +| `Linear.Team.Read.Find` +| `team(id: $id) { ... }` — with `Team.full_fields/0` + +| `Project` +| `projects` +| `:all` +| read +| `Linear.Project.Read.All` +| `projects(first:, after:)` — paginated with `Project.base_fields/0` + +| `Project` +| `my_projects` +| `:mine` +| read +| `Linear.Project.Read.Mine` +| `team(id:) { projects }` per user team, fanned out concurrently + +| `Project` +| `projects_by_team` +| `:by_team` +| read +| `Linear.Project.Read.ByTeam` +| `team(id: $teamId) { projects(first:, after:, filter:) { ... } }` — paginates the selected team's projects, or applies a server-side name/slug/id filter when `search` is supplied + +| `Project` +| `create_project` +| `:create` +| create +| `Linear.Project.Create` +| `projectCreate(input: { name, teamIds })` mutation + +| `Project` +| `find_project_by_name` +| `:by_name` +| read (`get?: true`) +| `Linear.Project.Read.ByName` +| `projects(filter: {name: {eq: $name}}, first: 1)` with `Project.fields_with_teams/0` + +| `Issue` +| `issues` +| `:list` +| read +| `Linear.Issue.Read.List` +| `issues(filter:, first:, after:)` paginated; `state` filters workflow-state types, + `status` filters friendly workflow-state names, `labels` filters by label name + (case-insensitive OR match via `IssueLabelCollectionFilter.some`); `include_labels` + (boolean, default `false`) selects label fields in the response without adding a + label filter — `labels` non-empty also forces label-field selection as a defensive + invariant; or `issue(id:)` per id (full, fanned concurrently) + +| `Issue` +| `create_issue` +| `:create` +| create +| `Linear.Issue.Create` +| `issueCreate(input: { title, description, teamId, labelIds?, projectId? })` mutation + +| `Issue` +| `assign_issue` +| `:assign` +| update +| `Linear.Issue.Update.Assign` +| `issueUpdate(id:, input: { assigneeId, stateId? })` via `Issue.Update.run/2` + +| `Issue` +| `attach_issue_to_project` +| `:attach_to_project` +| update +| `Linear.Issue.Update.AttachToProject` +| `issueUpdate(id:, input: { projectId })` via `Issue.Update.run/2` + +| `Issue` +| `close_issue` +| `:close` +| update +| `Linear.Issue.Update.Close` +| `issueUpdate(id:, input: { stateId })` via `Issue.Update.run/2`, then +`issueArchive(id:, trash: true)` when requested + +| `Issue` +| `set_issue_status` +| `:set_status` +| update +| `Linear.Issue.Update.SetStatus` +| `issueUpdate(id:, input: { stateId })` via `Issue.Update.run/2` + +| `IssueRelation` +| `issue_relations` +| `:list` +| read +| `Linear.IssueRelation.Read.List` +| `issue(id: $issueId) { relations(first:, after:) { edges { node { ... } cursor } pageInfo { ... } } }` and same for `inverseRelations`, fetched concurrently; paginated up to 100 per direction. Each relation node's `issue` and `relatedIssue` endpoints are fetched with `IssueRelation.endpoint_fields/0` (`id identifier title url state { name }`) — the nested `state { name }` field was added to support the `--graph` dependency graph display. + +| `IssueRelation` +| `create_issue_relation` +| `:create` +| create +| `Linear.IssueRelation.Create` +| `issueRelationCreate(input: { issueId, relatedIssueId, type })` mutation; returns `issueRelation { id type issue { ... } relatedIssue { ... } }`. Arguments: `issue_id` (string), `related_issue_id` (string), `type` (string, wire GraphQL enum value: `blocks`, `related`, `duplicate`). The `:create` action always receives the wire-direction values; `blocked-by` reversal happens in the CLI layer. Duplicate relation errors from Linear are surfaced to the CLI as `{:duplicate_relation, message}`. + +| `IssueRelation` +| `delete_issue_relation` +| `:destroy` +| destroy +| `Linear.IssueRelation.Destroy` +| `issueRelationDelete(id: $id)` mutation returning `DeletePayload { success entityId }`. Takes an `IssueRelation` struct as its argument; the relation UUID is read from `changeset.data.id`. The CLI layer resolves the exact relation (by listing the subject issue's relations and matching by wire type, direction, and endpoint identifier) before calling this interface, so no resolution logic lives in the domain layer. + +| `Label` +| `labels_by_names` +| `:by_names` +| read +| `Linear.Label.Read.ByNames` +| `issueLabels(filter: { name: { in: $names } })` edges + +| `Label` +| `labels_by_team` +| `:by_team` +| read +| `Linear.Label.Read.ByTeam` +| `team(id: $teamId) { labels(first: 100, filter: ...) }` — server and client filtered + +| `WorkflowState` +| `workflow_states_by_team` +| `:by_team` +| read +| `Linear.WorkflowState.Read.ByTeam` +| `team(id: $teamId) { states { nodes { ... } } }` + +| `Comment` +| `add_comment` +| `:create` +| create +| `Linear.Comment.Create` +| `commentCreate(input: { issueId, body })` mutation + +| `ProjectUpdate` +| `post_project_update` +| `:create` +| create +| `Linear.ProjectUpdate.Create` +| `projectUpdateCreate(input: { projectId, body, health? })` mutation + +|=== + +== Shared helpers + +=== `LinearCli.Linear.Issue.Update` + +`app/lib/linear_cli/linear/issue.ex` — not an Ash module; a plain +module with a single `run/2` function. + +All four issue-update actions (`:assign`, `:attach_to_project`, `:close`, +`:set_status`) delegate to this shared runner rather than each building +their own `issueUpdate` mutation. `run/2` calls the mutation, receives +the updated issue map, and decodes it via `Issue.from_map/1` using +`Issue.full_fields/0` (the full fragment including assignee, team, +comments, and labels). + +The mutation GraphQL document is built at call time (not a module +attribute) because `Issue.full_fields/0` references `User`, `Team`, and +`Comment` — cross-file module-attribute evaluation order is not guaranteed +at compile time. + +=== `LinearCli.Linear.IssueRelation.endpoint_fields/0` and `relation_fields/0` + +`app/lib/linear_cli/linear/issue_relation.ex` — module-level helpers that +return GraphQL field-selection strings, used everywhere a relation node is +fetched. + +`endpoint_fields/0` returns `"id identifier title url state { name }"` — +the fragment for each issue endpoint (`issue` and `relatedIssue`) inside a +relation node. The `state { name }` subfield was added to support the +`--graph` dependency graph display, which needs workflow status for each node. + +`relation_fields/0` composes `endpoint_fields/0` into the full relation +node fragment: `"id type issue { ... } relatedIssue { ... }"`. + +Both functions are evaluated at call time (not module attributes) to avoid +compile-time cross-file evaluation ordering issues. + +=== `LinearCli.CLI.Commands.Issues.Graph` + +`app/lib/linear_cli/cli/commands/issues/graph.ex` — pure BFS traversal +that builds a transitive dependency graph from a root issue identifier. + +`build/2` accepts `(root_identifier, root_issue)` where `root_issue` is the +already-fetched `Issue` struct (reusing the `issue view` fetch to avoid a +redundant API call). It traverses `blocks` relations in both directions via +`Linear.issue_relations/1`, visiting each identifier at most once (cycle-safe), +and returns `{:ok, %{root:, nodes:, edges:}}` or `{:error, {identifier, reason}}`. +Capped at 100 nodes; deterministic output (nodes sorted by identifier, edges +by source then target). + +=== `LinearCli.Linear.Paginate` + +`app/lib/linear_cli/linear/paginate.ex` — a standalone helper module. + +Provides `all/5`: fetches cursor-paginated GraphQL connections +(`edges { node { ... } cursor } pageInfo { hasNextPage endCursor }`) +until `max` records are collected or the API signals no more pages. +Used by: + +* `Team.Read.All` — `teams(first:, after:)` paginated query +* `Project.Read.All` — `projects(first:, after:)` paginated query +* `Issue.Read.List` — `issues(filter:, first:, after:)` paginated list + (the find-by-ids path fans out individual `issue(id:)` calls instead) + +== Maintenance contract + +*This document must be updated in the same change as any of the following:* + +* A new Ash resource is added to `LinearCli.Linear` (or an existing one + is removed or renamed). +* A new action is added to any resource, or an existing action's name, + type, arguments, or manual module changes. +* A new domain code interface is defined (or an existing one is removed + or renamed). +* An association attribute (`:term` or `{:array, :term}` field holding + another resource's struct) is added, removed, or renamed on any + resource. +* A shared helper (`Issue.Update`, `Paginate`, or any future equivalent) + is added, removed, or materially changes its role. +* Any declared Ash `relationships do` block is added to any resource + (currently none exist; the distinction from GraphQL/nested-data + associations must be kept accurate). + +Historical rationale for changes belongs in the relevant phase-plan +document under `documents/`, not here. This document records current +state only. diff --git a/.ai/skills/documents/assets/example-decision-burrito-decision.adoc b/.ai/skills/documents/assets/example-decision-burrito-decision.adoc new file mode 100644 index 0000000..07da482 --- /dev/null +++ b/.ai/skills/documents/assets/example-decision-burrito-decision.adoc @@ -0,0 +1,72 @@ += Burrito distribution decision +Tj Vanderpoel (bougyman) +:revdate: Sep 04, 2026 +: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::[] + +== Status + +Active. The project ships Burrito-wrapped binaries for macOS Apple Silicon, +Linux x86_64/aarch64, and Windows x86_64. + +== Decision + +Continue using Burrito for end-user distribution. + +The primary product requirement is the easy-button installation experience: +a user downloads/extracts a small platform-native release archive (or uses +Homebrew/install.sh), puts its `lc` executable on `PATH`, and runs it. The +executable carries the Erlang runtime, so users do not need to install Erlang, +Elixir, or a project toolchain. The same Linux executable is also the payload +for the daemon container. + +A standard `mix release` can include ERTS and therefore does not inherently +require a user-installed Erlang runtime. It would, however, ship an unpacked +release directory/tarball and need a separate user-facing launcher for this +CLI. That is a worthwhile fallback, but materially less direct than the +single-executable path. + +== Cost and safeguards + +Burrito adds release-engineering work: native NIF compatibility, embedded +ERTS/musl behavior, cross-platform CI, and occasional upstream workarounds. +This cost is accepted because the distribution experience is important. + +The release pipeline must continue to prove the supported artifacts work: + +* build each target on a compatible native runner; +* assert every required release artifact exists; +* smoke-test Linux binaries with the native Markdown/NIF runtime loaded; +* retain the Linux loader isolation regression coverage; and +* treat a Burrito upgrade or a newly added native dependency as a + release-platform change, requiring smoke testing on every supported target. + +== When to revisit + +Reconsider Burrito in favor of native per-platform `mix release --tar` +artifacts when one or more of these becomes true: + +* maintaining Burrito patches or workarounds repeatedly blocks releases or + creates unacceptable security risk; +* a supported target or new native dependency cannot be packaged and tested + reliably; or +* the project no longer values the single-executable installation experience + enough to justify its maintenance cost. + +Any replacement must preserve the current practical guarantees: no required +Erlang/Elixir install for users, a documented `lc` launcher and wrapper +scripts, checksummed release assets, and a viable daemon-container build. + +== History + +The initial packaging decision and empirical `exqlite` cross-build validation +are recorded in link:phase-8-plan.adoc[Phase 8]. Later Burrito-specific +hardening belongs in the commits and tests that implement it; this document +records the continuing product decision and its review criteria. diff --git a/.ai/skills/plain-english b/.ai/skills/plain-english new file mode 120000 index 0000000..34d7feb --- /dev/null +++ b/.ai/skills/plain-english @@ -0,0 +1 @@ +../../vendor/claude-plain-english-skill/skills/plain-english \ No newline at end of file diff --git a/.ai/skills/simple-english b/.ai/skills/simple-english new file mode 120000 index 0000000..168706a --- /dev/null +++ b/.ai/skills/simple-english @@ -0,0 +1 @@ +../../vendor/claude-plain-english-skill/skills/simple-english \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000..6838a11 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.ai/skills \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 6fe41cb..9f8b405 100644 --- a/.gitmodules +++ b/.gitmodules @@ -42,3 +42,6 @@ path = vendor/linear-cli url = git@github.com:rubyists/linear-cli branch = main +[submodule "vendor/claude-plain-english-skill"] + path = vendor/claude-plain-english-skill + url = git@github.com:b1rdmania/claude-plain-english-skill diff --git a/AGENTS.md b/AGENTS.md index b3e46e9..e1e96d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,10 +33,24 @@ Use `mise exec -- mix lc [LC_ARGS...]` for Linear operations. The task delegates Fantasia issues belong to the `EXT` team and the `Fantasia` project. -## Planning documents +## Documents + +Use the link:vendor/claude-plain-english-skill/skills/simple-english[`simple-english`] skill when +authoring technical documentation or instructions. + +Use the link:vendor/claude-plain-english-skill/skills/plain-english[`plain-english`] skill when +authoring non-technical documentation, marketing copy, or other content intended for a general audience. + +Do not apply both to the same text. Use `plain-english` for general audience content and `simple-english` +for technical content. + +### Planning documents - `documents/plans/pre-phase-1-plan.adoc` describes the vendored Python bootstrap. - `documents/plans/initial-plan.adoc` describes the initial Elixir port and Fantasia release. + +### Decision documents + - `documents/decisions/repository-management-mix-tasks-decision.adoc` records where repository-wide Mix tasks live and defines their discovery contract. - `documents/decisions/0008-managed-toolchain-refresh.adoc` records the accepted mise dependency refresh diff --git a/vendor/claude-plain-english-skill b/vendor/claude-plain-english-skill new file mode 160000 index 0000000..1e1501e --- /dev/null +++ b/vendor/claude-plain-english-skill @@ -0,0 +1 @@ +Subproject commit 1e1501e0e4da21d63b6611e2f258cf28c356a86c