{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://raw.githubusercontent.com/yahyabedirhan/shipyard/main/schema/config.schema.json", "title": "shipyard configuration", "description": "config.toml for shipyard, the macOS menu bar app for the pull requests, issues and workflow runs on your projects. The file lives in $XDG_CONFIG_HOME/shipyard/ (default ~/.config/shipyard/). Every key is optional; a missing key takes its default, so an empty file means defaults with no projects. Keys are kebab-case. Shipyard applies edits live, keeps the last valid configuration when an edit is broken, and ignores unknown keys with a warning.", "type": "object", "additionalProperties": false, "properties": { "version": { "description": "The configuration format version. Only 1 exists. Set it in every file that sets a key: a file without it reads with a warning, and from version 2 it is an error.", "type": "integer", "enum": [1], "default": 1 }, "theme": { "description": "The theme the panel draws with, by name, ignoring case: a built-in theme (Shipyard, Native, Graphite, Paper, Midnight or Harbor) or the name of a theme file in the themes folder beside config.toml. An unknown name is an error; a theme whose text has too little contrast on its panel reads with a warning.", "type": "string", "examples": ["Shipyard", "Native", "Graphite", "Paper", "Midnight", "Harbor"], "default": "Shipyard" }, "refresh-interval": { "description": "How often to refresh: a whole number and one unit, s, m, h or d, such as \"2m\", at least 30 seconds. A floor: shipyard stretches it when a refresh at this interval would spend more than max-share-percent of a GitHub rate limit.", "type": "string", "pattern": "^0*([3-9][0-9]|[1-9][0-9]{2,})s$|^0*[1-9][0-9]*[mhd]$", "default": "2m" }, "launch-at-login": { "description": "Start shipyard when you log in.", "type": "boolean", "default": true }, "menu-bar": { "description": "What the menu bar shows next to the icon.", "type": "object", "additionalProperties": false, "properties": { "count": { "description": "The attention count: one total, split by kind (pull requests, issues, runs), or no number.", "type": "string", "enum": ["total", "per-kind", "none"], "default": "total" } } }, "menu": { "description": "How the panel draws your projects.", "type": "object", "additionalProperties": false, "properties": { "layout": { "description": "One scrolling list of every project, one line per item under pinned project headers, or one project at a time in tabs.", "type": "string", "enum": ["list", "tabs"], "default": "list" }, "header-counts": { "description": "The kinds with a count in each project header of the list layout, each listed once. A project's header shows a count for each listed kind it has items of. The counts keep their own order: pull requests, issues, workflow runs, pings, notes. Every project shares this list.", "type": "array", "items": { "type": "string", "enum": ["pull-requests", "issues", "workflow-runs", "pings", "notes"] }, "uniqueItems": true, "default": ["pull-requests", "issues", "workflow-runs", "pings", "notes"] } } }, "rate-limit": { "description": "How much of your GitHub rate limit shipyard may use, and whether the panel shows it. The limit is shared with every other tool using your account, your agents included.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "Show the rate limit in the panel footer always, only when it's low (below 25%), or never.", "type": "string", "enum": ["always", "when-low", "never"], "default": "always" }, "max-share-percent": { "description": "The largest share of each hourly GitHub limit (GraphQL points, REST requests) shipyard spends, in percent.", "type": "integer", "minimum": 1, "maximum": 50, "default": 10 } } }, "attention": { "description": "Which reasons make an open item need attention (count in the menu bar). Clicking an item clears all of them until it changes again. Closed items never need attention.", "type": "object", "additionalProperties": false, "properties": { "unseen": { "description": "An item you haven't clicked yet.", "type": "boolean", "default": true }, "changed": { "description": "An item that changed (commits, comments, reviews, checks, review request) since you clicked it.", "type": "boolean", "default": true }, "review-requested": { "description": "A pull request that requests your review.", "type": "boolean", "default": true }, "checks-failed": { "description": "A pull request whose checks failed.", "type": "boolean", "default": true } } }, "herdr": { "description": "How a ping's Herdr action (shipyard ping send --herdr) takes you there: clicking it focuses the Herdr tab, then brings this terminal forward.", "type": "object", "additionalProperties": false, "properties": { "terminal": { "description": "The terminal app Herdr runs in, by name (\"Ghostty\") or bundle id (\"com.mitchellh.ghostty\"), brought forward after the tab is focused. Unset: only the tab is focused.", "type": "string", "minLength": 1 } } }, "remote": { "description": "Your other machines, whose agents' pings the menu lists beside this Mac's. Shipyard asks each one through Herdr's connection to it (herdr --machine), never over SSH of its own.", "type": "object", "additionalProperties": false, "properties": { "machines": { "description": "The machines to ask for their pings, by the labels your Herdr knows them by as saved machines (\"beta-vps\"): never a host or an address. Each machine's pings list in a section named after it, after the projects. A label can't start with \"-\" or be a project's slug or title.", "type": "array", "items": { "type": "string", "pattern": "^\\s*[^\\s-]" }, "uniqueItems": true, "default": [] } } }, "notices": { "description": "Notices from your other machines' agents (shipyard notice send), straight to this Mac over your tailnet. Off by default: nothing listens unless listen is true. The app listens on 127.0.0.1 only and takes only notices sent from this Mac's own Tailscale login; you expose the port with tailscale serve, and each other machine names this Mac in its own cli.toml.", "type": "object", "additionalProperties": false, "properties": { "listen": { "description": "Whether the app listens for notices from your other machines.", "type": "boolean", "default": false }, "port": { "description": "The port on 127.0.0.1 the app listens on, which tailscale serve exposes to your tailnet: tailscale serve --bg --http= http://127.0.0.1:.", "type": "integer", "minimum": 1, "maximum": 65535, "default": 47420 } } }, "banners": { "description": "The banners above the projects in the panel.", "type": "object", "additionalProperties": false, "properties": { "snooze-duration": { "description": "How long a banner stays hidden after you click its dismiss button: a whole number and one unit, s, m, h or d, such as \"1h\" or \"10s\". After it, the banner shows again if its condition still holds.", "type": "string", "pattern": "^[1-9][0-9]*[smhd]$", "default": "1h" } } }, "defaults": { "description": "What every project shows and notifies about, unless the project overrides it.", "type": "object", "additionalProperties": false, "properties": { "group-by": { "$ref": "#/definitions/group-by" }, "subsections": { "$ref": "#/definitions/subsections" }, "sort-by": { "$ref": "#/definitions/sort-by" }, "show-first": { "$ref": "#/definitions/show-first" }, "pull-requests": { "$ref": "#/definitions/pull-requests" }, "issues": { "$ref": "#/definitions/issues" }, "workflow-runs": { "$ref": "#/definitions/workflow-runs" }, "pings": { "$ref": "#/definitions/pings" }, "notes": { "$ref": "#/definitions/notes" }, "notifications": { "description": "The notification rules for every project that doesn't list its own, and the only rules for app control's control.started and control.ended. Default: a new pull request by anyone, a new ping, an agent's notice, and an agent starting and ending its lease on the app.", "$ref": "#/definitions/notifications", "default": [ { "event": "pr.opened", "authors": [] }, { "event": "ping.sent", "authors": [] }, { "event": "agent.notice", "authors": [] }, { "event": "control.started", "authors": [] }, { "event": "control.ended", "authors": [] } ] }, "archived": { "$ref": "#/definitions/archived" }, "forks": { "$ref": "#/definitions/forks" } } }, "projects": { "description": "One [[projects]] block per project, shown as a section in this order.", "type": "array", "items": { "$ref": "#/definitions/project" }, "default": [] } }, "definitions": { "group-by": { "description": "What a project's items are grouped by, one level: kind (pull requests, then issues, then runs), repository (A to Z), date (Today, Yesterday, This week, This month, Older, by the sort-by date; updated when sorting by title), author (A to Z; pings by who sent them), or none (one list).", "type": "string", "enum": ["kind", "repository", "date", "author", "none"], "default": "kind" }, "subsections": { "description": "Draw each group under a subheader (its name and count) instead of after a divider line. Unset, each layout keeps its own look: dividers in the list, subheaders in tabs. A value set applies to both layouts.", "type": "boolean" }, "sort-by": { "description": "The order within a group: last updated (newest first), created (newest first) or title (A to Z). Open or running items always come before closed or finished ones.", "type": "string", "enum": ["updated", "created", "title"], "default": "updated" }, "show-first": { "description": "How many rows each group shows before a \"Show N more\" row, which reveals the rest and then reads \"Show less\". 0 shows every row. With group-by = \"none\" it caps the whole project. A group's count includes the rows it hides; the menu shows every cap again each time it opens.", "type": "integer", "minimum": 0, "default": 0 }, "archived": { "description": "Whether repository groups and owner/* bring in archived repositories. A repository named as owner/name is always listed.", "type": "boolean", "default": false }, "forks": { "description": "Whether repository groups and owner/* bring in forks. A repository named as owner/name is always listed.", "type": "boolean", "default": true }, "project": { "description": "A named group of one or more GitHub repositories, shown as one section. Tables here merge key by key onto [defaults]; notifications replaces the default list.", "type": "object", "additionalProperties": false, "required": ["repositories"], "anyOf": [{ "required": ["slug"] }, { "required": ["name"] }], "properties": { "slug": { "description": "The project's ID, unique across projects: lowercase letters and digits joined by single hyphens, such as \"e-commerce\". Required. shipyard ping send --project and shipyard notice send --project find a project by its slug, then by its title.", "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" }, "title": { "description": "The text the menu shows for the project: its section's and its tab's title. Unset: the slug.", "type": "string", "pattern": "\\S" }, "repositories": { "description": "The project's repository selectors, each listed once: \"owner/name\" (one repository, always listed, archived or not), \"owner/*\" (every repository a user or organization owns), or a repository group: \"owned\" (your own account's), \"organizations\" (reached through an organization you're a member of, teams included), \"collaborator\" (someone else's that added you), \"anywhere\" (open pull requests waiting on your review in any repository, from GitHub's search; the project must set pull-requests.review-requested = true and show no issues or runs). Groups and wildcards are looked up again at least hourly, so new repositories appear on their own; archived and forks decide what they bring in. Shipyard also rejects two spellings that differ only in case, which this check can't see.", "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "pattern": "^([A-Za-z0-9-]+/([A-Za-z0-9._-]+|\\*)|owned|organizations|collaborator|anywhere)$" } }, "group-by": { "$ref": "#/definitions/group-by" }, "subsections": { "$ref": "#/definitions/subsections" }, "sort-by": { "$ref": "#/definitions/sort-by" }, "show-first": { "$ref": "#/definitions/show-first" }, "archived": { "$ref": "#/definitions/archived" }, "forks": { "$ref": "#/definitions/forks" }, "pull-requests": { "$ref": "#/definitions/pull-requests" }, "issues": { "$ref": "#/definitions/issues" }, "workflow-runs": { "$ref": "#/definitions/workflow-runs" }, "pings": { "$ref": "#/definitions/pings" }, "notes": { "$ref": "#/definitions/notes" }, "notifications": { "description": "This project's notification rules; replaces [[defaults.notifications]]. An empty list means no notifications for this project.", "$ref": "#/definitions/notifications" } } }, "pull-requests": { "description": "Pull requests: open ones, then those closed within the closed window, newest first, from the authors authors allows.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List pull requests.", "type": "boolean", "default": true }, "states": { "description": "Which pull requests to list: open (drafts too), merged, closed (without merging). Merged and closed ones stay for closed-window. Those left out are never shown, counted or notified.", "type": "array", "items": { "type": "string", "enum": ["open", "merged", "closed"] }, "default": ["open", "merged", "closed"] }, "closed-window": { "description": "How long closed and merged pull requests stay listed: a whole number and one unit, s, m, h or d, such as \"30m\" or \"7d\"; \"0\" hides them.", "type": "string", "pattern": "^(0|[0-9]+[smhd])$", "default": "7d" }, "drafts": { "description": "List draft pull requests (in gray).", "type": "boolean", "default": true }, "authors": { "$ref": "#/definitions/authors" }, "review-requested": { "description": "List only open pull requests waiting on your review, requested from you or from one of your teams.", "type": "boolean", "default": false } } }, "issues": { "description": "Issues: open ones, then those closed within the closed window, from the authors authors allows.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List issues.", "type": "boolean", "default": false }, "states": { "description": "Which issues to list: open, closed. Closed ones stay for closed-window. Those left out are never shown, counted or notified.", "type": "array", "items": { "type": "string", "enum": ["open", "closed"] }, "default": ["open", "closed"] }, "closed-window": { "description": "How long closed issues stay listed: a whole number and one unit, s, m, h or d, such as \"30m\" or \"7d\"; \"0\" hides them.", "type": "string", "pattern": "^(0|[0-9]+[smhd])$", "default": "7d" }, "authors": { "$ref": "#/definitions/authors" } } }, "workflow-runs": { "description": "GitHub Actions runs: running now, plus finished within the finished window.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List workflow runs.", "type": "boolean", "default": false }, "states": { "description": "Which workflow runs to list: in-progress (queued or running), failed (timed out and failed to start too), succeeded. Finished ones stay for finished-window. Those left out are never shown, counted or notified.", "type": "array", "items": { "type": "string", "enum": ["in-progress", "failed", "succeeded"] }, "default": ["in-progress", "failed", "succeeded"] }, "finished-window": { "description": "How long finished runs stay listed: a whole number and one unit, s, m, h or d, such as \"30m\" or \"3h\"; \"0\" hides them. Running ones always are.", "type": "string", "pattern": "^(0|[0-9]+[smhd])$", "default": "3h" }, "branches": { "description": "Which branches' runs to list: the default branch and open pull request branches, or all branches.", "type": "string", "enum": ["default-and-pull-requests", "all"], "default": "default-and-pull-requests" }, "authors": { "$ref": "#/definitions/authors" } } }, "pings": { "description": "The pings agents send with the shipyard CLI (shipyard ping), each filed under a project. Pings take no states, authors, drafts or review-requested.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List pings.", "type": "boolean", "default": true }, "seen-window": { "description": "How long a seen ping stays listed, counted from when it was seen: a whole number and one unit, s, m, h or d, such as \"30m\" or \"24h\"; \"0\" lets it leave at once. An unseen ping stays until it's seen.", "type": "string", "pattern": "^(0|[0-9]+[smhd])$", "default": "24h" } } }, "notes": { "description": "Your own notes, kept in Notion: one database per project, titled with the project's name, under the \"Shipyard Notes\" page. The menu lists a project's open notes, newest first, read through ntn, Notion's CLI, once it's installed and logged in to your notes workspace. Notes take only show.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List notes.", "type": "boolean", "default": true } } }, "authors": { "description": "Whose items this kind lists: those show matches (empty: everyone) minus those hide matches; hide wins. Items left out are never shown, counted or notified. A project's authors merges key by key onto the defaults'.", "type": "object", "additionalProperties": false, "properties": { "show": { "description": "List only these authors' items; empty lists everyone.", "$ref": "#/definitions/author-selectors", "default": [] }, "hide": { "description": "Never list these authors' items, even when show matches them.", "$ref": "#/definitions/author-selectors", "default": [] } } }, "author-selectors": { "description": "Author selectors: the groups me (you, and your agents working as you), others (anyone but you and bots) and bots (a Bot account or a [bot] login), or one login written with @, e.g. \"@dependabot[bot]\". A bare word is always a group.", "type": "array", "items": { "type": "string", "pattern": "^(me|others|bots|@[A-Za-z0-9][A-Za-z0-9-]*(\\[bot\\])?)$" } }, "notifications": { "description": "Notification rules: a macOS notification is sent for an event on an item the project lists when a rule names it and its authors match.", "type": "array", "items": { "$ref": "#/definitions/notification-rule" } }, "notification-rule": { "type": "object", "additionalProperties": false, "required": ["event"], "properties": { "event": { "description": "The change to notify about. agent.notice is an agent's notice (shipyard notice send), refused for a project whose rules leave it out. control.started and control.ended (an agent starting and ending its lease on the app) are decided by the top-level rules only: a project's rule for one is ignored.", "type": "string", "enum": [ "pr.opened", "pr.merged", "pr.closed", "pr.reopened", "pr.review_requested", "pr.checks_failed", "pr.commented", "issue.opened", "issue.closed", "issue.commented", "run.failed", "run.succeeded", "ping.sent", "agent.notice", "control.started", "control.ended" ] }, "authors": { "description": "Whose items the rule covers, as author selectors (me, others, bots or an @login); empty is everyone. It only narrows what the project lists. A ping or a notice has no author, so a rule with authors never selects one, and authors are ignored on control.started and control.ended.", "$ref": "#/definitions/author-selectors", "default": [] } } } } }