/** * The settings key TABLE (RT-47/RT-50): rt's rows, plus the suite rows * (deck/board/gitq/mattstack/claude) — the machinery in registry-machinery.ts * that reads and validates against this table does not change when rows are * added. * * Suite rows omit `migrated` (see registry-machinery.ts's docblock): they * carry no rt-legacy file to migrate from, so `isMigrated()` treats the * absent flag as resolver-backed from day one. */ import type { SettingDef, SettingScope } from "./registry-machinery.ts"; import { NOTIFICATION_DEFAULTS } from "./notification-events.ts"; const ALL_SCOPES: SettingScope[] = ["user", "team", "machine"]; const ROWS: readonly SettingDef[] = [ // --- migrated:true (wave 1) --------------------------------------------- { key: "rt.roles", type: "object", scopes: ALL_SCOPES, default: {}, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, pathGuardFields: ["hook"], description: "Per-repo dev-role definitions: port pools, env passthrough, and the dev-server hook command.", }, { key: "rt.intercepts", type: "array", scopes: ALL_SCOPES, default: [], merge: "replace", repoScoped: true, repoOnly: true, migrated: true, description: "Per-repo endpoint intercept rules consumed by rt intercept install.", }, { key: "rt.worktrees", type: "object", scopes: ALL_SCOPES, default: { onDeck: 0 }, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, description: "Per-repo worktree pool config (onDeck size, ready steps, name pool, staleClaimDays, junk (untracked globs a merged tree may discard on dispose)); root/branchFormat/ready computed-or-empty in the reader.", }, { key: "rt.worktreeApp", type: "object", scopes: ["team", "user", "machine"], default: { enabled: false, killProcesses: true }, merge: "deep", migrated: true, description: "Worktree pool on/off switch (enabled, killProcesses, claudeHook), merged per field. A team can turn the pool on for every member; a user or machine value overrides the team's, so a machine that wants it off sets {\"enabled\":false} at machine scope. claudeHook records this machine's answer to the Claude hook offer and is read from the machine store only. A distinct key from rt.worktrees (the per-repo pool config above).", }, { key: "rt.worktreeMergeCleanupNotice", type: "boolean", scopes: ALL_SCOPES, default: true, merge: "replace", repoScoped: true, repoOnly: true, description: "Say when a repo's merged worktrees are not cleaned up on their own (no PR checks, or no forge token). false hides that notice for the repo, in the Worktrees window and in rt worktree list.", }, { key: "rt.worktreeReadyApproval", type: "string", scopes: ALL_SCOPES, defaultNote: "Nobody has approved a ladder yet.", merge: "replace", repoScoped: true, repoOnly: true, migrated: true, description: "Per-repo user approval of a team-authored `ready` shell ladder, as its content hash (RT-89). The reader trusts only user/machine scopes so a team store can never approve its own shell; a hash mismatch after a team edit re-holds the ladder until `rt worktree ready-approve` records the new one.", }, { key: "rt.ignoredMrs", type: "object", scopes: ALL_SCOPES, default: { targetBranches: [], authors: [] }, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, description: "Per-repo MRs the daemon never tracks: {targetBranches?: glob[], authors?: username[]}. Target-branch globs are expanded to the repo's exact branch names and excluded on the GitLab request itself; authors are dropped after fetching. Absent means nothing is ignored.", }, { key: "rt.repoIdentityOverrides", type: "object", scopes: ["machine"], defaultNote: "No overrides, so repo names come from the remote.", merge: "replace", migrated: true, description: "Map of observed remote URL to pinned repo identity, for forks/multi-remote repos on this machine.", }, { key: "rt.repoRoots", type: "array", scopes: ["machine"], default: [], merge: "replace", migrated: true, description: 'Directories rt scans for git repos (rt cd, run-outside-a-repo pickers). Entries may start with "~/" or use "${home}". One level deep, plus worktree-pool parent folders one level deeper.', }, { key: "rt.notifications", type: "object", scopes: ["user"], merge: "deep", migrated: true, default: NOTIFICATION_DEFAULTS, description: "Desktop notification preferences (which events notify, sound on/off). Every event defaults to on; a user layer turns single events off.", }, { key: "rt.notify.eventBridges", type: "array", scopes: ["user"], default: [], merge: "replace", description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?, owner?, surface?}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload, plus the computed `{question}` field (the event payload's first question label, `payload.questions[0].label`, empty string when absent); `{headline}` and `{summary}` read the payload's own field when present, else a non-empty `payload.meta.headline` / `payload.meta.summary`, else `label` / the first question label, so a gate opener can supply its own notification copy; optional subjectPrefix matches the event payload's subject as a prefix. The optional url is interpolated the same way as title/message and becomes the notification's Open target; a gate rule should set it. The optional owner field (only literal \"human\" is valid) suppresses events whose payload.owner starts with \"herd:\", allowing gate rules to skip herd-owned events. The optional surface field (a string) matches only events whose payload.origin.surface equals the given value, allowing a rule to fire only for gates opened by a specific surface (e.g. surface: \"board\" matches only board-originated gates). A typical setup pairs a gate/opened rule with owner: \"human\" (human-owned gates notify) and a herd/gates-waiting/* rule with title \"{headline}\", message \"{summary}\" and no url (one summary per herd of worker gates its shepherd has left waiting past rt.gates.escalationTtlMinutes; the click focuses the shepherd pane). gate/escalated/ fires per gate and is not meant for a notification rule. A board-specific rule can combine surface: \"board\" with a board URL to open the decision queue directly.", }, { key: "rt.notify.usageLimits", type: "object", scopes: ["team", "user"], default: { thresholds: [], accounts: {} }, merge: "deep", description: "When the daemon notifies about a Claude account's usage. Off until thresholds are set: thresholds are percentages of any limit, such as [80, 95] (monthly spend, 5-hour, weekly, per-model); accounts overrides them per account, keyed by email or org UUID, and an empty list silences that account; hint is appended to every notification, such as where to ask for more usage. Usage comes from every account cswap manages, when it is installed, plus the account Claude Code is signed into when cswap does not list it.", }, { key: "rt.notify.sounds", type: "object", scopes: ["machine"], default: { neutral: "original", positive: "original", warning: "original" }, merge: "deep", description: "Which sound the mattstack app plays on this Mac for each kind of notification: neutral, positive (passed, approved, merged) and warning (failed, conflicts). Each is a sound id: original (that tier's own tone), sober/101 to sober/104, macos/ for a sound in /System/Library/Sounds, custom/ for a file added in Settings > Notifications, or none for silence. Set it from the app's Notifications tab.", }, { key: "rt.cron", type: "object", scopes: ["machine"], default: { triggers: [] }, merge: "deep", migrated: true, description: "Commands the daemon runs when a matching event is broadcast, each on its own debounce. The daemon picks up a change within 30 seconds of its next event.", }, { key: "rt.repoTracking", type: "object", scopes: ["machine"], default: {}, merge: "deep", migrated: true, description: "Which repos rt tracks for background sync and status polling.", }, { key: "rt.runsPruneDays", type: "number", scopes: ["machine"], default: 30, merge: "replace", migrated: true, description: "Age floor in days for pruning finished pipeline run directories under ~/.mattstack/runs (default 30).", }, { key: "rt.runaway", type: "object", scopes: ["machine"], default: { cpuThreshold: 80, sustainMs: 300000, graceMs: 120000 }, merge: "deep", migrated: true, description: "Thresholds for the runaway-process guard that kills stuck dev servers. Restart the daemon to apply changes.", }, { key: "rt.workspacePrefs", type: "object", scopes: ["machine"], default: {}, merge: "deep", migrated: true, description: "Per-machine editor/terminal preferences applied when opening a worktree.", }, { key: "rt.homeSnapshot", type: "object", scopes: ["machine"], default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30 }, merge: "deep", migrated: true, description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long.", }, { key: "rt.teamSnapshot", type: "object", scopes: ["machine"], default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30, pullIntervalSec: 300 }, merge: "deep", migrated: true, description: "Team-clone snapshot daemon config: the home snapshot's fields plus pullIntervalSec, the fast-forward/rebase pull cadence for every clone under ~/.mattstack/orgs.", }, { key: "rt.sync", type: "object", scopes: ALL_SCOPES, default: { autoResolve: [] }, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, description: "Branch sync behavior: fast-forward rules and stale-branch handling.", }, { key: "rt.branchNaming", type: "object", scopes: ALL_SCOPES, merge: "deep", repoScoped: true, default: { template: "${identifier}-${titleSlug}" }, repoOnly: true, description: "Branch-naming template for a repo, read by the VS Code extension (extensions/vscode/rt-context). rt itself has no CLI-side reader.", }, { key: "rt.variations", type: "object", scopes: ALL_SCOPES, default: {}, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, description: "Named parameter sets rt run can pick between for a command.", }, { key: "rt.presets", type: "object", scopes: ALL_SCOPES, default: {}, merge: "deep", repoScoped: true, repoOnly: true, migrated: true, description: "Saved argument presets for frequently repeated rt commands, keyed by name.", }, { key: "rt.dopplerTemplate", type: "array", scopes: ALL_SCOPES, defaultNote: "No template, so Doppler sync skips this repo.", merge: "replace", repoScoped: true, repoOnly: true, migrated: true, description: "Template used to generate a repo's Doppler secrets config.", }, // --- wave 2 (ports from rt's own files, RT-53) --------------------------- { key: "sdm.resources", type: "object", scopes: ["team"], default: {}, merge: "replace", description: "Per-resource overrides for the rt sdm picker: resource name -> label/tier/production/reasonSuggestion/db. Team-only, since it names employer resources.", }, { key: "sdm.carriers", type: "object", scopes: ["team"], default: {}, merge: "replace", description: "Display names for StrongDM tenant tags in the rt sdm picker: tenant tag -> { label }. A tag with no entry shows capitalised. Team-only, since it names employer customers.", }, { key: "rt.logRetentionDays", type: "number", scopes: ["machine", "user"], default: 14, merge: "replace", migrated: true, description: "Age floor in days for the log janitor pruning every surface's rotated log files under ~/.mattstack/rt/logs (default 14).", }, { key: "rt.gitStatus", type: "object", scopes: ALL_SCOPES, default: { sweep: true, sweepIntervalSec: 300, fetchIntervalSec: 900 }, merge: "deep", repoScoped: true, migrated: true, description: "Mission-control git badge sweep. sweep gates the daemon sweep and fetchIntervalSec (the background fetch cadence, 0 disables fetching) are both read per repo, so a per-repo override of either takes effect (a repo override of { sweep: false } opts that repo out). sweepIntervalSec, the minimum seconds between sweeps, is read only from the global config by the sweep tick; a per-repo override of sweepIntervalSec has no effect.", }, { key: "rt.evidence.foldScreenshots", type: "boolean", scopes: ALL_SCOPES, default: true, merge: "replace", repoScoped: true, description: "Fold each case's screenshots in the evidence section rt writes into an MR or PR description, behind a summary that names the case, so reviewers scroll past them to the approvals and pipeline.", }, { key: "review.ruleSelector", type: "boolean", scopes: ["org", "team", "user", "machine"], default: false, merge: "replace", description: "Turns on the team pack's review rule selector. The pack's rule-selector fill reads it on every review.", }, { key: "rt.logLevel", type: "string", scopes: ["machine", "user"], default: "info", merge: "replace", migrated: true, description: "Daemon log level (trace|debug|info|warn|error). RT_LOG_LEVEL env wins, then this setting, then info (lib/daemon-logger.ts resolveDaemonLogLevel).", }, { key: "rt.apiPort", type: "number", scopes: ["machine", "user"], default: 9401, merge: "replace", migrated: true, description: "TCP port for the daemon's local HTTP/WS API. Escape hatch when 9401 is held: RT_API_PORT env wins, then this setting, then 9401 (lib/daemon-config.ts resolveApiPort(), read at bind time by lib/daemon/api-server.ts).", }, { key: "rt.hooks", type: "object", scopes: ALL_SCOPES, default: { enabled: true }, merge: "deep", repoScoped: true, repoOnly: true, description: "Per-repo git hook switches ({enabled, hooks: {: boolean}}); a hook the map does not name runs. rt hooks writes your user settings, so a repo's choices follow you to every Mac. rt's git hook shim asks rt for this value before each hook and runs the hook unless it is off.", }, { key: "rt.daemonPath", type: "string", scopes: ["machine"], defaultNote: "Your login shell's PATH.", merge: "replace", description: "Absolute colon-separated PATH the daemon uses for every child it spawns, instead of probing your login shell. Set this when the daemon can't find node/git/bun/pnpm (e.g. a fish shell, a blocking .zshrc, or PATH exports that live only in .zshrc). Machine-scoped: it never travels to another machine.", }, { key: "rt.trustedBrowserOrigins", type: "array", scopes: ["user", "machine"], default: [], merge: "replace", description: "Browser Origins (scheme://host:port, exact string match) trusted to read the :9401 daemon API and subscribe to /ws without presenting the local api-token -- e.g. a locally-hosted console or chat-viewer dev server. Empty by default: every current mattstack consumer (the CLI, the Swift tray, rt-client from Bun/Node processes, the VS Code extension) is a non-browser client (sends no Origin header at all) and is unaffected either way.", }, { key: "rt.mcp.uploadRoots", type: "array", scopes: ["machine"], default: [], merge: "replace", description: "Absolute directories the mr_upload MCP tool may read files from, beside its built-in roots (the target repo's worktrees and this user's Claude Code temp root). Machine-only: path literals never travel. A non-absolute entry is ignored with a warning.", }, { key: "rt.picker.hidden", type: "array", scopes: ["user", "machine"], default: [ "agent", "apps", "bg", "chat", "ci", "cron", "daemon", "deps", "endpoint", "events", "gate", "herd", "home", "intercept", "pane", "plugin", "progress", "reconciler", "release", "runs", "secrets", "services", "settings", "setup", "skills", "state", "team", "telemetry", "tools", "uninstall", "verify", "settings source-path", "settings schema", "settings test-push", "settings extension", ], merge: "replace", description: "Verbs left out of rt's pickers, usage and --help, by path (\"pane\", or \"worktree provision\" for a nested verb). The default hides the verbs the mattstack apps, skills and daemon run; edit it to show one of those or hide any other verb, and [] lists everything. Hidden verbs still run by name, and `rt --all` lists everything once.", }, { key: "rt.ui.background", type: "string", scopes: ["user"], default: "auto", merge: "replace", description: "Whether rt's output uses its colors for a dark or a light terminal: auto, dark or light. Auto reads COLORFGBG and Ghostty's config, then asks the terminal for its background, and uses the light-terminal colors when none of them answers. Set dark or light when auto picks wrong, or to keep rt from asking the terminal.", }, // --- mattstack (installer-lane) ----------------------------------------- { key: "mattstack.integrations", type: "object", scopes: ["team"], default: {}, merge: "deep", description: "Team-wide external integration config (forge, slack app, linear workspace) the installer provisions; a team's Linear key is in mattstack.directory; client secrets never live here.", }, { key: "mattstack.tracking", type: "object", scopes: ["team"], default: {}, merge: "deep", description: "Team-declared repo tracking intent, identity-keyed; the daemon layers it under machine-scoped rt.repoTracking, which wins per repo whenever it names that repo at all — including an explicit local {mode:\"off\"} entry, the way to opt a repo out of team-declared tracking.", }, { key: "mattstack.appPath", type: "string", scopes: ["machine"], defaultNote: "Found in Applications or your home Applications folder.", merge: "replace", description: "Absolute path to the installed mattstack.app bundle, written by the app at launch so rt stops hardcoding ~/Applications.", }, { key: "setup.waived", type: "array", scopes: ["machine"], default: [], merge: "replace", description: "Finish-gated setup rows the user skipped on this Mac through `rt setup waive` (today only tool.fast-browser-extension); the wizard's Finish no longer waits on them. Machine-only: a loaded Chrome extension is a per-profile fact and the choice is per machine, so it never travels with a team or user store.", }, { key: "rt.integrations", type: "object", scopes: ["user"], defaultNote: "No forge host confirmed yet.", merge: "deep", migrated: true, description: "User-confirmed integration hosts (forgeHost), written by an explicit `rt setup gitlab connect --host` after that host validates. The one trusted source a credential is ever sent to: mattstack.integrations' team-declared host is shown to the user but never auto-used for a credentialed fetch.", }, // --- mattstack (shared team truth) --------------------------------------- { key: "mattstack.roster", type: "array", scopes: ["org"], default: [], merge: "replace", description: "Everyone in the org: [{username, name?, agePublicKey?, teams?}], by forge username. teams lists the team folders a member belongs to. Any app that lists people reads this; hiding someone is the app's own overlay (e.g. boxscore.hiddenMembers).", }, { key: "mattstack.org", type: "object", scopes: ["org"], default: { owners: [], admins: [], teams: {} }, merge: "replace", description: "Who runs the org: its owners (who manage admins; with none listed, the admins are owners), its admins, and each team's owners, by forge username.", }, { key: "mattstack.directory", type: "object", scopes: ["org"], default: {}, merge: "replace", description: "Every team the apps should know about, on mattstack or not: each team's Linear key and Slack channels (the channel other teams ask it in for code owner review, plus channels by kind). Keyed by team name; a name matching a team folder is that mattstack team.", }, { key: "mattstack.activeTeam", type: "string", scopes: ["user"], defaultNote: "Your first team on the roster.", merge: "replace", description: "The team you use the apps as, when the roster lists you on more than one.", }, // --- claude (installer-lane) -------------------------------------------- { key: "claude.marketplaces", type: "array", scopes: ["user", "team"], default: [], merge: "add", description: "Claude Code plugin marketplaces to add, in order. The org's, the team's and your own lists add up.", }, { key: "claude.plugins", type: "array", scopes: ["user", "team"], default: [], merge: "add", description: "Claude Code plugins to install, in order. The org's, the team's and your own lists add up; a plugin from the org or a team is installed but left for you to enable.", }, // --- deck (MAT-384 settings half) --------------------------------------- { key: "deck.apps", type: "object", scopes: ["user"], default: {}, merge: "deep", description: "Per-app deck publish state (published flag, publicFollowsOverride); password hashes and session secrets stay out of this store.", }, { key: "deck.access", type: "object", scopes: ["user"], default: {}, merge: "deep", description: "deck's sign-in rules, per app.", }, { key: "deck.platform", type: "object", scopes: ["machine"], default: { publicDomain: null, legacyPrefixes: [], railway: null, tunnel: null }, merge: "deep", description: "deck's platform-level machine config: public domain and legacy URL prefixes; Cloudflare secrets stay out of this store.", }, // --- board (team) -------------------------------------------------------- { key: "board.gitlabHost", type: "string", scopes: ["team"], defaultNote: "Required: the team's GitLab host.", merge: "replace", description: "GitLab host the board polls for MRs, shared by the whole team.", }, { key: "board.projects", type: "array", scopes: ["team"], defaultNote: "Required: the projects the board tracks.", merge: "replace", description: "GitLab projects the board tracks, shared by the whole team.", }, { key: "board.title", type: "string", scopes: ["team"], default: "MRs ready for review", merge: "replace", description: "Display title shown in the board's UI.", }, { key: "board.botUsernames", type: "array", scopes: ["team"], default: [], merge: "replace", description: "GitLab usernames the board treats as bots, excluded from human MR attribution.", }, { key: "board.ticketPrefixes", type: "array", scopes: ["team"], defaultNote: "The active team's Linear key.", merge: "replace", description: "Ticket key prefixes (e.g. RT, MAT) that keep the authors tab to the team's own tickets and link out to Linear from an MR title; rows on a codeowners tab pass regardless of prefix.", }, { key: "board.slack", type: "object", scopes: ["team"], default: { reviewKind: "review", singleTemplate: "{title}: {url}", multiHeader: "{count} MR's ready for review :pray:", multiItem: "- {title}: {url}", autoResolveIntervalMinutes: 15, emoji: { looking: "eyes", commented: "speech_balloon", approved: "white_check_mark" }, emojiAliases: {} }, merge: "deep", description: "The board's Slack posting config (app id, client id, templates, callback port, reviewKind). The team's review channel is in mattstack.directory. Client secrets stay out of this store.", }, { key: "board.doctorSkill", type: "string", scopes: ["team"], default: "", merge: "replace", description: "Default doctor skill for repairing a stuck MR; a repo's skills.jsonc doctor slot overrides it when present.", }, { key: "board.triage.doctorSkill", type: "string", scopes: ["team"], default: "", merge: "replace", description: "Doctor skill the board's own API-tier triage sweep runs on your MRs; deliberately never resolved through a repo's skills.jsonc manifest. A sibling flat key of board.triage, not a field inside it — the board reader assembles the two independently.", }, { key: "board.tabs", type: "array", scopes: ["team"], default: [{ id: "team", label: "Team", source: { kind: "authors" } }], merge: "replace", storeVersion: 2, description: "Board tabs ({id, label, source, slackChannel?, reviewSkill?, pack?, dimEmpty?}), at least one, each id unique; pack picks which team pack's bindings a launch from that tab uses; dimEmpty fades roster members with nothing on that tab (absent: every tab but an authors one fades). source.kind 'authors' is the classic roster board; 'codeowners' lists any author's open MRs carrying a CODE_OWNER rule for the section, until merge/close. With nothing set, one Team tab of the roster's MRs.", }, { key: "board.turn", type: "object", scopes: ["team", "user"], default: { author: ["threads", "changesRequested", "conflicts", "rebase", "ciFailing", "readyToMerge"], reviewer: ["assigned", "approvalReset", "repliedThreads"] }, merge: "replace", description: "What makes an open MR someone's turn. author: blockers that make it the author's move (the board's 'Waiting on author' Show item, Needs me respond/fix/merge). reviewer: what puts it in a reviewer's Needs me queue. An absent list means every signal (fallback lives in the board reader).", }, // --- board (user) ---------------------------------------------------------- { key: "board.staleAfterDays", type: "number", scopes: ["user"], defaultNote: "Each project's MR sync window.", merge: "replace", description: "Days of MR inactivity before the board flags it stale, for this developer. Absent = follow each project's rt MR sync window.", }, { key: "board.gateGraceMinutes", type: "number", scopes: ["user"], default: 90, merge: "replace", description: "Minutes an unanswered review gate stays open before the board parks the review (closes the pane, keeps the gate open).", }, { key: "board.workspaces", type: "object", scopes: ["user"], default: { reviews: "reviews", responds: "responses", doctors: "doctors" }, merge: "deep", description: "Herdr workspace names the board's review/respond/doctor panes launch into, per developer.", }, { key: "board.defaultMember", type: "string", scopes: ["user"], default: "all", merge: "replace", description: "Which board member identity this developer's local board runs as by default.", }, { key: "board.hiddenMembers", type: "array", scopes: ["user"], default: [], merge: "replace", description: "Usernames this developer hides from the authors tab's roster; overlays the team truth without editing it.", }, { key: "board.triage", type: "object", scopes: ["user"], default: { enabled: false, cooldownMinutes: 30, dailyAttemptBudget: 3, fixClasses: { retryFlake: true, inheritedNoteDraft: true, cleanApiRebase: false, mechanicalLint: false, codeFix: false }, tier: "api", notify: "rt" }, merge: "deep", description: "This developer's triage user-intent flags (which triage sweeps run automatically); a sibling flat key of board.triage.doctorSkill, not its container — the board reader assembles the two independently.", }, { key: "board.reReview", type: "object", scopes: ["user", "team"], merge: "deep", default: { enabled: true }, description: "Gate for the board's automatic re-review sweep ({enabled}); rt's cron.triage step installs the board-triage trigger only while this is on.", }, { key: "board.peerAsks", type: "object", scopes: ["user", "team"], merge: "deep", default: { enabled: true }, description: "Whether teammates' boards can ask this board's agent for a review, re-review or replies ({enabled}). On, asks wait in the asks inbox for a go-ahead unless the asker is in board.peerAsksAlwaysAllow; off, this board leaves every ask picker and declines what still arrives. rt's cron.triage step installs the board-peer trigger only while this is on. Auto-doctor stays under board.triage.", }, { key: "board.peerAsksAlwaysAllow", type: "array", scopes: ["user"], default: [], merge: "replace", description: "Teammates (usernames) whose asks for this board's agent start without waiting for a go-ahead. Everyone else's ask waits in the asks inbox.", }, { key: "board.codeowners", type: "object", scopes: ALL_SCOPES, default: { slack: { fromSectionName: false } }, merge: "deep", repoScoped: true, repoOnly: true, description: "What the board does with one repo's Code Owner sections, a property per feature. slack.fromSectionName says the repo's section names carry the owning team's Slack channel (Acme - #pod-acme), which turns on posting an MR to the channels of the owners who have not approved it.", }, // --- board (machine) --------------------------------------------------- { key: "board.agent.account", type: "string", scopes: ["user", "machine"], defaultNote: "Your default Claude profile.", merge: "replace", description: "cswap account the board's review/respond/doctor panes launch under; unset uses the default claude profile. Replaces the retired board.claudeCommand.", }, { key: "board.agent.model", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Claude uses its own.", merge: "replace", description: "Default --model for the board's review/respond/doctor panes; unset omits the flag.", }, { key: "board.agent.effort", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Claude uses its own.", merge: "replace", description: "Default --effort for the board's review/respond/doctor panes; unset omits the flag.", }, { key: "board.cwds", type: "object", scopes: ["machine"], default: {}, merge: "deep", description: "Local working directories the board's review/respond/doctor panes launch from.", }, { key: "board.triageMaxConcurrent", type: "number", scopes: ["machine"], default: 2, merge: "replace", description: "Max concurrent triage panes the board launches on this machine.", }, // --- boxscore ------------------------------------------------------------- { key: "boxscore.projects", type: "array", scopes: ["team"], defaultNote: "Required: the projects boxscore scores.", merge: "replace", description: "GitLab projects boxscore scores, as full paths (\"group/project\").", }, { key: "boxscore.linearDoneStates", type: "array", scopes: ["team"], default: [], merge: "replace", description: "Linear workflow state names that count as done. Empty means the completed and canceled state types.", }, { key: "boxscore.sizeBand", type: "object", scopes: ["team"], default: { tooSmall: 10, tooLarge: 400 }, merge: "deep", description: "MR size health band in changed lines: {tooSmall, tooLarge}. At or below tooSmall, or above tooLarge, is outside the healthy band.", }, { key: "boxscore.excludeFilePatterns", type: "array", scopes: ["team"], default: [], merge: "replace", description: "Glob patterns for files excluded from addition/deletion counts (e.g. \"**/*.json\").", }, { key: "boxscore.ignoredMrs", type: "array", scopes: ["team"], default: [], merge: "replace", description: "MRs excluded from all metrics: \"!123\" or \"group/project!123\".", }, { key: "boxscore.botPatterns", type: "array", scopes: ["team"], default: [], merge: "replace", description: "Extra regex sources treated as bot accounts, beyond boxscore's built-in detection.", }, { key: "boxscore.hiddenMembers", type: "array", scopes: ["user"], default: [], merge: "replace", description: "Usernames from mattstack.roster hidden from this developer's leaderboard.", }, { key: "boxscore.roles", type: "object", scopes: ["team"], default: {}, merge: "replace", description: "Who sees the whole team in boxscore: { \"\": \"team\" | \"self\" }. A roster member not listed sees only their own page. An org admin, or an owner of the team the Mac works as, always sees the whole team. A courtesy boundary, not a security one: each member's boxscore runs on their own Mac.", }, { key: "boxscore.defaultRange", type: "string", scopes: ["user"], default: "30d", merge: "replace", description: "Default comparison window for the API and CLI when none is given: \"7d\", \"30d\", or \"90d\".", }, // --- gitq ------------------------------------------------------------------ { key: "gitq.workSlots", type: "object", scopes: ["machine"], default: { workSlotLocation: "auto", maxWorkSlots: 3 }, merge: "deep", description: "gitq's local work-slot config: on-disk location and the max slot count.", }, { key: "gitq.forges", type: "object", scopes: ["user"], default: {}, merge: "deep", description: "gitq's host-keyed forge config, tokenEnv names only — never a live token.", }, { key: "gitq.board", type: "object", scopes: ["machine"], defaultNote: "Set up by rt setup with the repos it finds.", merge: "deep", description: "gitq checkout-board config: tracked repos, local port, and the herdr workspace it launches into.", }, // --- chat (RT-48 Task 7) ------------------------------------------------- { key: "chat.handle", type: "string", scopes: ["user"], defaultNote: "Taken from the pane title, the repo, or this Mac.", merge: "replace", description: "Explicit rt chat handle for this developer on this machine; overrides the derived - handle when set.", }, { key: "chat.humanHandle", type: "string", scopes: ["user"], default: "matt", merge: "replace", description: "The human's own chat handle, so agents can @-mention them by name.", }, { key: "chat.viewerUrl", type: "string", scopes: ["user"], merge: "replace", default: "https://chat.mattstack", description: "Base URL of the chat viewer, no trailing slash; rt chat post and the tail's wake lines end with a link to the room or message. Defaults to the viewer deck serves on the mattstack TLD.", }, { key: "chat.herdrWorkspace", type: "string", scopes: ["user"], default: "chat", merge: "replace", description: "herdr workspace label that rt pane spawn opens new agent tabs in; created when missing.", }, { key: "chat.autoSignIn", type: "boolean", scopes: ALL_SCOPES, default: false, merge: "replace", repoScoped: true, repoOnly: true, description: "Sign every new Claude Code session in a herdr pane in to rt chat and its repo room as it starts. Headless runs never sign in.", }, { key: "chat.push.provider", type: "string", scopes: ["user"], defaultNote: "Push alerts are off.", merge: "replace", description: "Push notification provider used to alert the human of chat mentions when away from a terminal.", }, { key: "chat.push.target", type: "string", scopes: ["user"], defaultNote: "Push alerts are off.", merge: "replace", description: "Destination (topic/URL/token) the configured chat.push.provider sends to.", }, // --- agent (rt agent handoff) -------------------------------------------- // agent.provider needs a concrete fallback to preserve claude-only behavior // with zero code changes for callers who never set it. { key: "agent.provider", type: "string", scopes: ["user", "machine"], default: "claude", merge: "replace", description: "Which provider rt agent start uses when --provider is not given: \"claude\" or \"codex\".", }, { key: "agent.claude.model", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Claude uses its own.", merge: "replace", description: "Default --model for claude rt agent launches; unset omits the flag.", }, { key: "agent.claude.effort", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Claude uses its own.", merge: "replace", description: "Default --effort for claude rt agent launches; unset omits the flag.", }, { key: "agent.claude.account", type: "string", scopes: ["user", "machine"], defaultNote: "Your default Claude profile.", merge: "replace", description: "cswap account email claude rt agent launches under; unset uses the default claude profile.", }, { key: "agent.claude.extraArgs", type: "string", scopes: ["user", "machine"], defaultNote: "No extra arguments.", merge: "replace", description: "Opaque extra claude arguments appended to every claude rt agent launch (escape hatch).", }, { key: "agent.claude.yolo", type: "boolean", scopes: ["user", "machine"], default: false, merge: "replace", description: "Default --yolo (--dangerously-skip-permissions) for claude rt agent launches; unset behaves as false.", }, { key: "telemetry.enabled", type: "boolean", scopes: ["user"], default: true, merge: "replace", description: "Send this Mac's mattstack versions to the mattstack switchboard about every 50 minutes, so the mattstack admin can see who is on which version.", }, { key: "agent.codex.model", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Codex uses its own.", merge: "replace", description: "Default -m/--model for codex rt agent launches; unset omits the flag.", }, { key: "agent.codex.effort", type: "string", scopes: ["user", "machine"], defaultNote: "Left out, so Codex uses its own.", merge: "replace", description: "Default reasoning effort for codex rt agent launches, passed as -c model_reasoning_effort=; unset omits the override.", }, { key: "agent.codex.extraArgs", type: "string", scopes: ["user", "machine"], defaultNote: "No extra arguments.", merge: "replace", description: "Opaque extra codex arguments appended to every codex rt agent launch (escape hatch).", }, { key: "agent.codex.yolo", type: "boolean", scopes: ["user", "machine"], default: false, merge: "replace", description: "Default --yolo (--dangerously-bypass-approvals-and-sandbox) for codex rt agent launches; unset behaves as false. codex has no per-agent account setting (see spec's Non-goals).", }, // --- gates (escalation) ---------------------------------------------------- { key: "rt.gates.escalationTtlMinutes", type: "number", scopes: ["user"], default: 10, merge: "replace", description: "Minutes an open herd-owned gate waits before the escalation sweep marks it escalated (topic gate/escalated/) and counts it toward the herd/gates-waiting/ summary, at most one per herd per herd.watchdog.notifyQuietMins. Fires on either trigger: the TTL elapses (reason \"ttl\"), or the owning herd's shepherd subscription is gone or dead before the TTL (reason \"owner-dead\"). 0 escalates any eligible gate on the first sweep after it opens.", }, { key: "ci.watch.budgetMinutes", type: "number", scopes: ALL_SCOPES, default: 75, merge: "replace", description: "Minutes a CI attendant watches one GitLab pipeline through ci_watch before it stops and asks (default 75), measured from the pipeline's own creation time; ci_watch reports budget.spent once it passes. The GitHub poll keeps its own fixed limit. A team sets the convention; a user or machine may override it.", }, { key: "ci.triage.realPatterns", type: "array", scopes: ["team", "org"], default: [], merge: "add", description: "Failure-log patterns that mark a CI failure as a real code defect, as case-sensitive JavaScript regular expressions that extend ci_triage's built-in list. A pattern containing [[: (a POSIX bracket class) is refused. The org holds what every team shares and a team adds its own.", }, { key: "ci.triage.infraPatterns", type: "array", scopes: ["team", "org"], default: [], merge: "add", description: "Failure-log patterns that mark a CI failure as infrastructure trouble rather than the change, as case-sensitive JavaScript regular expressions that extend ci_triage's built-in list. A pattern containing [[: (a POSIX bracket class) is refused. The org holds what every team shares and a team adds its own.", }, { key: "ci.triage.noisyJobs", type: "array", scopes: ["team", "org"], default: [], merge: "add", description: "CI jobs known to fail for unrelated reasons, so ci_triage treats their failures as noise. Each entry matches any job whose name contains it, and only jobs allowed to fail. The org holds what every team shares and a team adds its own.", }, { key: "ci.triage.baseDepth", type: "number", scopes: ["team", "org"], default: 3, merge: "replace", description: "How many pipelines back on the base branch ci_triage looks to tell a failure the branch caused from one the base already had (1 to 20). The org holds what every team shares and a team may set its own.", }, { key: "ci.triage.baseRef", type: "string", scopes: ["team", "org"], defaultNote: "Falls back to the MR's target branch.", merge: "replace", description: "The branch ci_triage compares a failing pipeline against. The org holds what every team shares and a team may set its own.", }, { key: "ci.triage.skipBase", type: "boolean", scopes: ["team", "org"], default: false, merge: "replace", description: "Turns off ci_triage's comparison against the base branch, so every failure is judged on its log alone. The org holds what every team shares and a team may set its own.", }, // --- herd (watchdog) --------------------------------------------------- { key: "herd.watchdog.enabled", type: "boolean", scopes: ["machine"], default: true, merge: "replace", description: "Turns the herd watchdog sweep on.", }, { key: "herd.watchdog.fastMins", type: "number", scopes: ["machine"], default: 2, merge: "replace", description: "Idle minutes before a worker with a pending DM, mention, or answered gate is poked.", }, { key: "herd.watchdog.shepherdFastMins", type: "number", scopes: ["machine"], default: 5, merge: "replace", description: "Minutes an open human-owned gate in the herd may stay unanswered before the shepherd is poked. Unread DMs and mentions for the shepherd wedge immediately and are not delayed by this key.", }, { key: "herd.watchdog.backstopMins", type: "number", scopes: ["machine"], default: 15, merge: "replace", description: "Idle minutes before any non-gated active worker is poked.", }, { key: "herd.watchdog.retryMins", type: "number", scopes: ["machine"], default: 5, merge: "replace", description: "Minutes between escalation strikes while the watchdog keeps poking an unresponsive worker.", }, { key: "herd.watchdog.notifyQuietMins", type: "number", scopes: ["machine"], default: 30, merge: "replace", description: "Minimum minutes between macOS notifications for the same herd.", }, { key: "herd.watchdog.nagMins", type: "number", scopes: ["machine"], default: 30, merge: "replace", description: "Minutes a done job may hold its pane before the shepherd is nagged to reclaim it.", }, { key: "herd.watchdog.notifyHuman", type: "boolean", scopes: ["machine"], default: true, merge: "replace", description: "Whether the watchdog's top escalation rung sends a macOS notification to the human.", }, { key: "herd.watchdog.midRunTrustAccept", type: "boolean", scopes: ["machine"], default: false, merge: "replace", description: "Whether the watchdog may drive a mid-run folder-trust dialog on a daemon-provisioned tree itself, rather than only parking the job and notifying (RT-196). Off by default: the screen match cannot yet confirm the dialog's folder matches the job's worktree, so a working session showing an unrelated permission prompt with the same shape is a real risk.", }, { key: "panes.relocationAutoAccept", type: "boolean", scopes: ["machine"], default: true, merge: "replace", description: "Whether the daemon may answer Claude Code's EnterWorktree permission-root relocation prompt on a blocked pane by itself (RT-200). Unlike the trust dialog, the prompt names the worktree path in its own body, and the daemon accepts only when that exact path is in rt's worktree registry, so this is on by default. Read by both the herd watchdog and the executor reconciler.", }, // --- skills (writing style) ---------------------------------------------- { key: "skills.writingStyle", type: "string", scopes: ["user", "team"], default: "mattstack:writing-style-conversational", merge: "replace", description: "Skill id that sets the voice for prose posted under your name (reviews, replies, PR descriptions). A team value is the default a member's user value overrides.", }, ]; /** `org` goes after `team` so a key's first scope, which the apps group their settings pages by, does not change. */ function withOrgScope(def: SettingDef): SettingDef { if (!def.scopes.includes("team") || def.scopes.includes("org")) return def; const at = def.scopes.indexOf("team") + 1; return { ...def, scopes: [...def.scopes.slice(0, at), "org", ...def.scopes.slice(at)] }; } export const REGISTRY: readonly SettingDef[] = ROWS.map(withOrgScope);