{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/prefs.schema.json",
  "title": "Multi-Agent Pipeline - multi-agent-preferences.json",
  "description": "User-level preferences. Written to $HOME/.claude/multi-agent-preferences.json. Never committed. Never synced to the open-source repo - may contain personal data.",
  "type": "object",
  "additionalProperties": false,
  "patternProperties": {
    "^_": {
      "description": "Documentation-only blocks shipped in preferences-template.json (_figmaConfigTemplate, _derivedSkillSourcesTemplate, _devToolkitTemplate). Copy-paste examples, never read by the pipeline."
    }
  },
  "required": ["schemaVersion", "global", "projects"],
  "properties": {
    "schemaVersion": {
      "type": "string",
      "enum": ["2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"],
      "description": "v2.0.0: pre-v3.7. v2.1.0: v3.7+ adds identities[].servicePatMap, platformIdentityRouting, recentGroups, recentBranches, serviceStatus, settings, expanded keychainMapping. v2.2.0: v6.0.0 formalizes v5.7 / v5.8 additions (reportChannels, reportContent with technicalAnalysis, wikiScope, autopilotReportTimeoutSeconds) that had been running as 2.1.0 sub-migrations without a proper version bump. v2.5.0: v14.0.0 adds global.skillConformance (Phase 4 criteria resolution) and declares global.ship.autoFix, which the tail command's spec had referenced as global.finish.autoFix without ever declaring it. v2.6.0: v15.0.0 renames global.ship to global.resumeLocal (/multi-agent:ship -> :resume-local)."
    },
    "global": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "identities": {
          "type": "array",
          "description": "Git identities the user can pick from in Phase 0. v2.1.0+: each identity may own per-service PAT keys via servicePatMap.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name", "email"],
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "format": "email"
              },
              "username": {
                "type": "string",
                "description": "SCM username (optional)."
              },
              "isDefault": {
                "type": "boolean"
              },
              "servicePatMap": {
                "type": "object",
                "additionalProperties": false,
                "description": "v2.1.0+. Maps a service name to the Keychain key name this identity owns for that service. Enables cross-identity isolation - corporate identity's Jira PAT is not accidentally used for personal repos.",
                "properties": {
                  "jira": {
                    "type": "string"
                  },
                  "bitbucket": {
                    "type": "string"
                  },
                  "github": {
                    "type": "string"
                  },
                  "confluence": {
                    "type": "string"
                  },
                  "figma": {
                    "type": "string"
                  },
                  "figma_mcp": {
                    "type": "string"
                  },
                  "firebase": {
                    "type": "string",
                    "description": "Firebase JSON (base64-encoded). project_id is parsed from the decoded JSON - no separate firebase_project entry."
                  },
                  "fortify": {
                    "type": "string"
                  },
                  "jenkins": {
                    "type": "string"
                  },
                  "figma_pat": {
                    "type": "string",
                    "deprecated": true,
                    "description": "DEPRECATED, pre-v13.6 name for `figma`. Declared only so a pre-migration identity still validates; `migrate-prefs.mjs` copies the value into `figma` and deletes this key."
                  },
                  "firebase_sa": {
                    "type": "string",
                    "deprecated": true,
                    "description": "DEPRECATED, early-v2.1.0 name for `firebase`. Declared only so a pre-consolidation identity still validates; `migrate-prefs.mjs` copies the value into `firebase` and deletes this key."
                  },
                  "firebase_project": {
                    "type": "string",
                    "deprecated": true,
                    "description": "DEPRECATED and unused. Declared only so a pre-consolidation identity still validates; `migrate-prefs.mjs` removes it."
                  }
                }
              }
            }
          }
        },
        "gitIdentities": {
          "type": "array",
          "deprecated": true,
          "description": "DEPRECATED in v2.1.0 - renamed to 'identities'. Migration script converts automatically. Kept in schema temporarily for migration compatibility.",
          "items": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "keychainMapping": {
          "type": "object",
          "additionalProperties": false,
          "description": "Maps logical token names to actual Keychain item names. Pipeline looks up tokens via `security find-generic-password -s <mapped-name>` (or `pipeline/lib/credential-store.sh get` cross-platform). Covered keys: jira, bitbucket, bitbucket_token, bitbucket_user, github, confluence, figma, figma_mcp, fortify, graylog, firebase, jenkins, npm, claude_oauth_token, claude_oauth_token_fallback, supabase_access, supabase_service_role. Deprecated and accepted only for backward compatibility: figma_pat (superseded by figma), figma_user (unused).",
          "properties": {
            "jira": {
              "type": ["string", "null"]
            },
            "bitbucket": {
              "type": ["string", "null"],
              "description": "Legacy alias for bitbucket_token."
            },
            "bitbucket_token": {
              "type": ["string", "null"]
            },
            "bitbucket_user": {
              "type": ["string", "null"]
            },
            "github": {
              "type": ["string", "null"]
            },
            "confluence": {
              "type": ["string", "null"]
            },
            "figma": {
              "type": ["string", "null"],
              "description": "Figma Personal Access Token. The canonical Tier 2 (REST API) key of the Figma access chain - this is the key `figma-token.sh` resolves. Supersedes `figma_pat`."
            },
            "figma_mcp": {
              "type": ["string", "null"],
              "description": "Figma MCP token. Tier 1 of the Figma access chain. The `mcp__claude_ai_Figma__*` calls themselves authenticate through Anthropic-managed MCP auth, but the token must be onboarded so that flow has something to bind to, and so `lib/figma-mcp-refresh.sh` can renew it."
            },
            "fortify": {
              "type": ["string", "null"]
            },
            "graylog": {
              "type": ["string", "null"]
            },
            "graylog_test": {
              "type": ["string", "null"],
              "description": "v15.15+ - Keychain key for the TEST Graylog instance. Null or absent falls back to the `graylog` key, which is correct for deployments that share one token across both."
            },
            "supabase_access": {
              "type": ["string", "null"],
              "description": "Supabase personal access token (Management API). Used by personal-site tooling; not consumed by pipeline phases."
            },
            "supabase_service_role": {
              "type": ["string", "null"],
              "description": "Supabase service_role key for the personal-site project. Server-side only; never shipped to a client."
            },
            "usage_ingest": {
              "type": ["string", "null"],
              "description": "Keychain item holding the ingest token for the optional operational reporting integration. When set, usage-report.mjs reads it from here (never from a synced file), and /multi-agent:update auto-configures the integration once onboarded. Absent = the integration stays off."
            },
            "figma_pat": {
              "type": ["string", "null"],
              "deprecated": true,
              "description": "DEPRECATED, pre-v13.6 name for `figma`. Kept so an un-migrated preferences file still validates; `migrate-prefs.mjs` copies the value into `figma` and deletes this key. Never write a new mapping here - the next migration removes it. Runtime lookups read `figma` first and fall back to this key only for installs that have not migrated yet."
            },
            "figma_user": {
              "type": ["string", "null"],
              "deprecated": true,
              "description": "DEPRECATED and unused. `migrate-prefs.mjs` removes this key; the REST flow authenticates with the PAT alone."
            },
            "claude_oauth_token": {
              "type": ["string", "null"],
              "description": "Claude Code OAuth token used by headless / scheduled runs."
            },
            "claude_oauth_token_fallback": {
              "type": ["string", "null"],
              "description": "Secondary OAuth token used when the primary is rate-limited or expired."
            },
            "firebase": {
              "type": ["string", "null"],
              "description": "Firebase JSON (base64-encoded). project_id is parsed from the decoded JSON."
            },
            "firebase_sa": {
              "type": ["string", "null"],
              "deprecated": true,
              "description": "DEPRECATED, early-v2.1.0 name for `firebase`. Declared only so a pre-consolidation preferences file still validates long enough for `migrate-prefs.mjs` to copy the value into `firebase` and delete this key. Never write a new mapping here."
            },
            "firebase_project": {
              "type": ["string", "null"],
              "deprecated": true,
              "description": "DEPRECATED and unused. Declared only so a pre-consolidation preferences file still validates; `migrate-prefs.mjs` removes it, because project_id is parsed from the decoded service-account JSON and needs no separate entry."
            },
            "jenkins": {
              "type": ["string", "null"]
            },
            "npm": {
              "type": ["string", "null"],
              "description": "NPM registry token (npm.pkg.github.com or npmjs.com). Used by package publish flow."
            },
            "appstore_connect_key_id": {
              "type": ["string", "null"],
              "description": "App Store Connect API key ID. Tier 1 of the App Store Connect access chain, used by /multi-agent:store-ready Gate 2 (and its iOS alias /multi-agent:testflight-validation). An identifier rather than a secret; mapped anyway so every credential is read through the same layer. Creating an API key needs an Admin or App Manager role, which is why Tier 2 exists."
            },
            "appstore_connect_issuer_id": {
              "type": ["string", "null"],
              "description": "App Store Connect API issuer ID. Required alongside appstore_connect_key_id; either alone leaves Tier 1 unusable and the resolver says so instead of silently dropping to Tier 2. The .p8 private key is a FILE and is never stored here - it belongs at ~/.appstoreconnect/private_keys/AuthKey_<keyId>.p8, one of the directories altool searches."
            },
            "appstore_connect_apple_id": {
              "type": ["string", "null"],
              "description": "Apple ID for Tier 2 of the App Store Connect access chain. Usable by any Apple ID holder with no elevated role, which is the realistic path when API-key creation is not permitted on the account."
            },
            "appstore_connect_password_item": {
              "type": ["string", "null"],
              "description": "Name of the keychain item holding the app-specific password, created by `altool --store-password-in-keychain-item`. Holds an ITEM NAME, not a password: altool reads the secret itself via `-p @keychain:<item>`, so the value never reaches an argument list or the pipeline."
            }
          }
        },
        "tokenScripts": {
          "type": "object",
          "additionalProperties": {
            "type": ["string", "null"]
          },
          "default": {},
          "description": "Maps service ids to user-owned token generation scripts (absolute paths, personal - never synced). Used by the Phase 0 token pre-flight: figma_mcp silent renewal reads the .figma-oauth.json next to tokenScripts.figma_mcp; the 'Regenerate now (script)' option runs the script itself."
        },
        "platformIdentityRouting": {
          "type": "object",
          "description": "v2.1.0+. Maps repo URL glob patterns to identity names. Used in Phase 0 to auto-select git identity + PAT based on repo origin. Prevents corporate identity leaking into personal repos and vice versa.",
          "additionalProperties": {
            "type": "string",
            "description": "An identity name present in identities[]."
          },
          "examples": [
            {
              "bitbucket.company.com/*": "work",
              "github.com/mmerterden/*": "personal"
            }
          ]
        },
        "recentProjects": {
          "type": "array",
          "description": "v2.1.0+ structured form. Last-used-first LRU of project slugs.",
          "items": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "additionalProperties": false,
                "required": ["path"],
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string"
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 0
                  },
                  "lastUsed": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            ]
          },
          "maxItems": 20
        },
        "recentBranches": {
          "type": "object",
          "description": "v2.1.0+. Per-project recent branch LRU. Keys are project paths or slugs. TTL 15 days (settings.branchTtlDays).",
          "additionalProperties": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": ["branch"],
              "properties": {
                "branch": {
                  "type": "string"
                },
                "count": {
                  "type": "integer",
                  "minimum": 0
                },
                "lastUsed": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            },
            "maxItems": 10
          }
        },
        "routines": {
          "type": "array",
          "description": "v2.4.0+. User-defined routines saved via /multi-agent:save. Each entry indexes a local-only /multi-agent:<name> command dir under ~/.claude/commands/multi-agent/. Never synced (personal). Managed by routine-registry.mjs.",
          "maxItems": 100,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["name", "description"],
            "properties": {
              "name": {
                "type": "string",
                "pattern": "^[a-z0-9][a-z0-9-]*$",
                "maxLength": 40,
                "description": "Routine command name; invocable as /multi-agent:<name>."
              },
              "description": {
                "type": "string",
                "description": "One-line summary shown by /multi-agent:routines and :help."
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "source": {
                "type": "string",
                "description": "Where the routine came from, e.g. a CLAUDE.md section name or 'user'."
              },
              "trigger": {
                "type": ["string", "null"],
                "description": "Optional natural-language trigger phrase the routine captures."
              }
            }
          }
        },
        "recentGroups": {
          "type": "array",
          "description": "v2.1.0+. LRU of multi-repo selection groups. Each group is a set of repo paths chosen together for multi-repo tasks. Max 10.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["repos"],
            "properties": {
              "label": {
                "type": "string",
                "description": "Optional human-readable name."
              },
              "repos": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "minItems": 2,
                "maxItems": 10
              },
              "count": {
                "type": "integer",
                "minimum": 0
              },
              "lastUsed": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "maxItems": 10
        },
        "multiRepoIntegrationHosts": {
          "type": "array",
          "description": "v5.6.0+. Learn-once registry of host projects that build together with a multi-repo combo (codegen producer \u2192 consumer + host integration project). Pipeline checks this registry in Phase 6 before commit; on match, auto-runs the host build. On miss for a \u22652-repo task, prompts the user once and persists the answer. See refs/multi-repo-integration-build.md for the full contract.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["repoSet"],
            "properties": {
              "repoSet": {
                "type": "array",
                "description": "Sorted array of repo names (just the basenames, not full paths) that trigger this host build when touched together.",
                "items": {
                  "type": "string"
                },
                "minItems": 2
              },
              "noHost": {
                "type": "boolean",
                "description": "True if the user explicitly said this combo has no host project. Presence of this field with true skips the build step entirely; other fields may be omitted."
              },
              "hostPath": {
                "type": "string",
                "description": "Absolute path to the host project that integrates the repoSet via submodules or package dependencies."
              },
              "platform": {
                "type": "string",
                "enum": ["ios", "android", "mixed"]
              },
              "hostScheme": {
                "type": "string",
                "description": "iOS: xcodebuild scheme. Android: gradle module."
              },
              "submodulePaths": {
                "type": "array",
                "description": "Relative submodule paths inside hostPath that must be refreshed before building.",
                "items": {
                  "type": "string"
                }
              },
              "resolveCommand": {
                "type": "string",
                "description": "Shell command run inside hostPath to flush package-manager caches (e.g. xcodebuild -resolvePackageDependencies)."
              },
              "buildCommand": {
                "type": "string",
                "description": "Shell command run inside hostPath to build the integration scheme/module. Pipeline greps stderr for 'error:' / 'FAILURE:' lines."
              },
              "lastUsed": {
                "type": "string",
                "format": "date-time"
              },
              "count": {
                "type": "integer",
                "minimum": 0,
                "description": "How many times this entry has fired. Used by autopilot to decide confidence level."
              },
              "lastResult": {
                "type": "string",
                "enum": ["success", "failed", "skipped"]
              }
            }
          },
          "maxItems": 20
        },
        "serviceStatus": {
          "type": "object",
          "description": "v2.1.0+. Ephemeral per-service reachability cache. TTL 5 minutes (settings.serviceStatusCacheSeconds). Each service is independent - Jira may be reachable while Bitbucket is not.",
          "additionalProperties": {
            "type": "object",
            "additionalProperties": false,
            "required": ["ok", "checkedAt"],
            "properties": {
              "ok": {
                "type": "boolean"
              },
              "checkedAt": {
                "type": "string",
                "format": "date-time"
              },
              "error": {
                "type": "string",
                "description": "Optional last-error message if ok=false."
              }
            }
          }
        },
        "settings": {
          "type": "object",
          "additionalProperties": false,
          "description": "v2.1.0+. Tunable behavior knobs. Defaults are used when missing.",
          "properties": {
            "branchTtlDays": {
              "type": "integer",
              "minimum": 1,
              "maximum": 365,
              "default": 15,
              "description": "How many days a branch stays visible in recentBranches picker before being filtered."
            },
            "pushRetryMax": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 5,
              "description": "Phase 6 push-must-succeed rebase-retry count before prompting user."
            },
            "buildRetryMax": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 3,
              "description": "Phase 3 build retry count before prompting user."
            },
            "serviceStatusCacheSeconds": {
              "type": "integer",
              "minimum": 60,
              "maximum": 3600,
              "default": 300,
              "description": "Per-service reachability cache TTL (seconds)."
            },
            "repoScanDays": {
              "type": "integer",
              "minimum": 7,
              "maximum": 365,
              "default": 90,
              "description": "Bitbucket/GitHub API repo-discovery filter: show repos active in the last N days."
            },
            "identityRoutingEnabled": {
              "type": "boolean",
              "default": false,
              "description": "When true, Phase 0 uses platformIdentityRouting to auto-select identity. When false, shows the identity picker."
            },
            "multiRepoEnabled": {
              "type": "boolean",
              "default": false,
              "description": "When true, Phase 0 project picker allows multi-select. When false, single-select only."
            },
            "pushMustSucceed": {
              "type": "boolean",
              "default": true,
              "description": "Phase 6 policy. When true, commit + push with rebase-retry up to pushRetryMax. When false, single push attempt, pause on fail."
            },
            "worktreeAutoRemoveOnPr": {
              "type": "boolean",
              "default": true,
              "description": "v14.1.0+ - Phase 6 policy. When true, a task's worktree is removed once its PR is open: artefacts (agent-state, triage output, .pipeline/, build+test logs) are salvaged into the log dir first, then `git worktree remove` runs. The BRANCH is kept and is NOT checked out, so the user's own HEAD and uncommitted work are untouched. Default true because the complaint it answers is forgetting to clean up, and every destructive path is gated: a worktree with real uncommitted changes, an unpushed HEAD, --local mode, or a cwd inside the worktree all skip with a reason. Set false to keep worktrees until /multi-agent:kill or :garbage-collect."
            }
          }
        },
        "defaultJiraKey": {
          "type": ["string", "null"],
          "description": "Default Jira project key used for placeholder replacement. null until /multi-agent:setup onboards a Jira project."
        },
        "figmaMcp": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "remoteGeneratorScript": {
              "type": ["string", "null"],
              "description": "Local-only path to a script that generates the remote MCP OAuth token (figu_) via Dynamic Client Registration + PKCE and saves it to the credential store. Run by the setup figma_mcp pass when the user picks Remote. Personal paths stay in prefs; never embedded in synced files."
            },
            "localGeneratorScript": {
              "type": ["string", "null"],
              "description": "Local-only path to a script that validates the Figma PAT and wires the PAT-based local MCP server (@anthropic-ai/figma-mcp) into Claude Code settings.json. Run by the setup figma_mcp pass when the user picks Local."
            }
          },
          "description": "Figma MCP onboarding helpers for /multi-agent:setup. Both fields optional; when unset the setup flow guides the token generation inline."
        },
        "modelFallback": {
          "type": "object",
          "additionalProperties": false,
          "description": "Model-tier fallback policy. When the premium tier is unavailable (quota, dispatch error, or an expired premium window) the pipeline drops to fallbackModel instead of failing the phase.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch for tier fallback."
            },
            "premiumTierUntil": {
              "type": ["string", "null"],
              "description": "ISO date after which the premium tier is assumed gone. null = no expiry known."
            },
            "fallbackModel": {
              "type": "string",
              "description": "Model id used when the premium tier is unavailable."
            },
            "onDispatchError": {
              "type": "boolean",
              "default": true,
              "description": "Also fall back when a dispatch call errors, not only on quota."
            }
          }
        },
        "hosts": {
          "type": "object",
          "additionalProperties": false,
          "description": "Corporate service hosts used by Jira / Confluence / Bitbucket / email-domain placeholders. Collected inline during Token Save Flow (Step 3.5) when a hosted-service token is added for the first time. Per-user, never committed - lives in ~/.claude/multi-agent-preferences.json only.",
          "properties": {
            "jira": {
              "type": "string",
              "description": "Jira host without scheme, e.g. jira.example.com. Resolves {JIRA_HOST}."
            },
            "confluence": {
              "type": "string",
              "description": "Confluence host without scheme, e.g. confluence.example.com. Resolves {CONFLUENCE_HOST}."
            },
            "bitbucket": {
              "type": "string",
              "description": "Bitbucket Server host without scheme, e.g. bitbucket.example.com. Resolves {BITBUCKET_HOST}."
            },
            "fortify": {
              "type": "string",
              "description": "Fortify SSC host without scheme, e.g. ssc.example.com. Used by Phase 0 Step 1b URL enrichment to validate pasted Fortify URLs and for direct SSC REST API calls. Resolves {FORTIFY_HOST}."
            },
            "graylog": {
              "type": "string",
              "description": "Graylog PRODUCTION host without scheme, e.g. logs.example.com. Used by the Graylog log-fetch adapter for direct REST API calls. Resolves {GRAYLOG_HOST}."
            },
            "graylogTest": {
              "type": "string",
              "description": "v15.15+ - Graylog TEST host without scheme. Optional; leaving it unset means fetch-graylog.sh only ever searches production. Test and production are separate instances, so a trx id minted by a tester does not exist in prod and searching prod alone answers 'no logs' for a complaint that is fully logged one host over. Resolves {GRAYLOG_TEST_HOST}."
            },
            "corpDomain": {
              "type": "string",
              "description": "Corporate email / cookie domain, e.g. example.com. Resolves {CORP_DOMAIN}."
            }
          }
        },
        "firebase": {
          "type": "object",
          "additionalProperties": false,
          "description": "v15.16+ - Firebase service-account keys. One team routinely owns several Firebase projects (a legacy app and its redesign, staging next to production), each with its own key, and the single keychainMapping.firebase slot forced a choice: a crash URL from the other project then failed the project_id check as if it were misconfigured. Leave this out entirely for a one-project setup - the single slot stays the fallback.",
          "properties": {
            "accounts": {
              "type": "array",
              "description": "Maps a Firebase projectId to the keychain key holding its service-account JSON. fetch-crashlytics.sh reads the projectId out of the console URL and picks the matching entry; no match falls back to keychainMapping.firebase.",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "required": ["projectId", "keychainKey"],
                "properties": {
                  "projectId": {
                    "type": "string",
                    "description": "Firebase project id exactly as it appears in a console URL, e.g. my-app-prod-1234."
                  },
                  "keychainKey": {
                    "type": "string",
                    "description": "Keychain key holding this project's service-account JSON (base64 or raw)."
                  },
                  "label": {
                    "type": "string",
                    "description": "Human label used in error output, e.g. \"redesign prod\". Defaults to the projectId."
                  }
                }
              }
            }
          }
        },
        "fortify": {
          "type": "object",
          "additionalProperties": false,
          "description": "v15.14+ - Fortify SSC behaviour. The host and the API token live in global.hosts.fortify and global.keychainMapping.fortify; this object holds the two things that are neither. Phase 4 Gate 5 read `fortify.alwaysCheck` from the moment it shipped, but global was closed to additional properties and this object did not exist, so setting it failed validation and the gate could only ever run off a referenced URL.",
          "properties": {
            "alwaysCheck": {
              "type": "boolean",
              "default": false,
              "description": "Run the Phase 4 Fortify gate on every run, not only when the task referenced a finding. Needs versionIds to know what to scan."
            },
            "versionIds": {
              "type": "array",
              "items": { "type": "string" },
              "default": [],
              "description": "SSC project version ids, searched in order. Used when a ticket names an issue instance id but no SSC URL (fetch-fortify.sh --instance-id), and by alwaysCheck. Ids are strings because SSC returns them as strings in some deployments."
            }
          }
        },
        "reportChannels": {
          "type": "object",
          "additionalProperties": false,
          "description": "v5.7+ - Phase 7 / /multi-agent:channels kanal se\u00e7imi default'lar\u0131. Multi-select men\u00fcde tick'li gelecek kanallar. Her kanal ba\u011f\u0131ms\u0131z boolean. Autopilot Phase 7'de ALWAYS pauses (30-min timeout) - bu de\u011ferler sadece men\u00fcn\u00fcn \u00f6nceden se\u00e7ili halini belirler.",
          "properties": {
            "pr": {
              "type": "boolean",
              "default": true,
              "description": "PR description update (replace/append). Default ON - en yayg\u0131n kanal."
            },
            "jira": {
              "type": "boolean",
              "default": true,
              "description": "Jira comment on linked issue. Default ON - task'a jiraId varsa tick'li gelir."
            },
            "confluence": {
              "type": "boolean",
              "default": false,
              "description": "Confluence page creation. Default OFF - bir kez parent page se\u00e7ince LRU'dan \u00f6ner."
            },
            "wiki": {
              "type": "boolean",
              "default": false,
              "description": "Component wiki pages (Case A scope multi-select). Default OFF - taskType=component + figmaConfig.wiki.enabled gerekli, yoksa men\u00fcde greyed out."
            }
          },
          "default": {
            "pr": true,
            "jira": true,
            "confluence": false,
            "wiki": false
          }
        },
        "reportContent": {
          "type": "object",
          "additionalProperties": false,
          "description": "v5.7+ - Phase 7 / /multi-agent:channels i\u00e7erik se\u00e7imi default'lar\u0131. Multi-select men\u00fcde tick'li gelecek content source'lar\u0131.",
          "properties": {
            "normalAnalysis": {
              "type": "boolean",
              "default": true,
              "description": "Phase 1+2+4 pipeline log'undan impact summary + risks + architectural decisions (y\u00fcksek seviye). Greyed out post-hoc \u00e7a\u011fr\u0131da pipeline log yoksa."
            },
            "technicalAnalysis": {
              "type": "boolean",
              "default": false,
              "description": "Changes (de\u011fi\u015fen dosyalar gruplan\u0131p ne/neden), Architecture (structural decisions), Dependencies (yeni import/framework/paket). PR body'deki 'Technical Details' b\u00f6l\u00fcm\u00fcn\u00fcn \u00f6zeti; user'\u0131n kanal se\u00e7imi PR i\u00e7ermedi\u011fi durumlarda (\u00f6r. sadece Jira/Confluence) teknik i\u00e7erik aktarmak istiyorsa devreye girer. Source: Phase 2 planning + Phase 3 dev log + PR diff stat."
            },
            "testScenarios": {
              "type": "boolean",
              "default": true,
              "description": "Precondition / steps / expected tablosu (4-8 sat\u0131r, user perspective). Pipeline log source."
            },
            "autoDiff": {
              "type": "boolean",
              "default": false,
              "description": "PR diff'ten auto-generate \u00f6zet (eski enrich behavior - root cause / solution / changed files / test scenarios). PR linked de\u011filse greyed out."
            },
            "manualNote": {
              "type": "boolean",
              "default": false,
              "description": "Serbest metin paragraf (--message / --message-file). Her durumda se\u00e7ilebilir."
            },
            "costSummary": {
              "type": "boolean",
              "default": false,
              "description": "v6.1.0+ - Per-phase token cost summary (phase name, tokens_in, tokens_out, est. USD). Source: phase-tracker.sh token tallies + optional OTel spans (MULTI_AGENT_OTEL_SPANS=1). Greyed out if tracker has no token data for the task. Opt-in - off by default so baseline PR body stays unchanged."
            },
            "workSummary": {
              "type": "boolean",
              "default": false,
              "description": "v7.1.0+ - Executive 'Work Done' summary block. Distills the whole pipeline run into a single-screen section: task + branch + base + PR number, scope delivered (\u2705/\u23f3 per Phase 2 task), changed files with +/- counts (capped at 20 rows), review outcome (accepted/deferred/rejected counts + approved flag), and a one-line phase tick strip (0 Init \u2705 \u00b7 1 Analysis \u2705 \u00b7 ...). Source: `agent-state.json` + `phase-tracker.json` + `git diff --numstat` between `baseBranch`...HEAD. Consumed by `render-work-summary.sh`. Greyed out if no state file exists for the task. Opt-in - off by default so baseline PR body stays unchanged."
            }
          },
          "default": {
            "normalAnalysis": true,
            "technicalAnalysis": false,
            "testScenarios": true,
            "autoDiff": false,
            "manualNote": false,
            "costSummary": false,
            "workSummary": false
          }
        },
        "autopilotReportTimeoutSeconds": {
          "type": "integer",
          "default": 1800,
          "minimum": 60,
          "maximum": 7200,
          "description": "v5.7+ - Phase 7'de autopilot always-pause men\u00fcs\u00fcnde kullan\u0131c\u0131 cevap vermezse session'\u0131 sonland\u0131rma s\u00fcresi (saniye). Default 1800 (30 dk). Timeout'ta external delivery aborted, internal capture (agent-log, telemetry, knowledge) yine \u00e7al\u0131\u015f\u0131r, session /multi-agent:resume ile devam ettirilebilir."
        },
        "wikiScope": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": ["main", "ios", "screenshots", "index"]
          },
          "default": ["main", "ios", "screenshots", "index"],
          "description": "v5.7+ - Wiki Case A scope multi-select default'u. Component wiki dispatch'inde hangi artifact'lar yaz\u0131lacak: main (ana component sayfas\u0131), ios (iOS sub-page), screenshots (assets/ klas\u00f6r\u00fc), index (_Sidebar.md + ComponentImplementationStatus.md). Legacy wikiDefault=true \u2192 [main,ios,screenshots,index] migration; wikiDefault=false \u2192 [] (empty array = Wiki adapter Case B men\u00fcs\u00fcne d\u00fc\u015fer)."
        },
        "autoJiraFromGithubIssue": {
          "type": "string",
          "enum": ["ask", "always", "never"],
          "default": "ask",
          "description": "Policy for the GitHub-issue Jira auto-create (WS-6 triad, Phase 0 Step 1). `ask` prompts the user when a GitHub issue has no Jira ID. `always` auto-creates silently (and patches the GitHub issue body with the link). `never` skips and uses the `feature/GH{issueNo}-kebab` branch. Autopilot treats `ask` as `always` - there is no interactive fallback when running unattended."
        },
        "wikiToJiraComment": {
          "type": "boolean",
          "default": true,
          "description": "When the Wiki channel produces component wiki content AND a Jira issue is linked (`state.jiraId` set), also post a humanizer-passed summary of the wiki content as a Jira comment so QA + PMs see the component docs inline. Independent of `reportChannels.jira` (which controls whether the main Jira comment runs). Set `false` to suppress only the wiki-summary comment."
        },
        "promptLanguage": {
          "type": "string",
          "enum": ["en", "tr"],
          "default": "en",
          "description": "Locked to 'en': the language of LLM-facing instructions (spec files, agent system prompts, reviewer/triage prompts) and picker structural chrome (AskUserQuestion label/header). What humans read is governed by outputLanguage instead - picker question/description, conversational lines, and external payload bodies (PR description, Jira comment, Confluence/Wiki). Always English regardless of either pref: commit messages, branch names, PR titles, code identifiers, agent-log.md. Canonical matrix: multi-agent-refs/rules.md 'Language Application'. Set via /multi-agent:language or /multi-agent:setup."
        },
        "outputLanguage": {
          "type": "string",
          "enum": ["en", "tr"],
          "default": "en",
          "description": "Language used for the assistant's NON-INTERACTIVE explanations, status updates, error messages, and pipeline-generated reports rendered to the user (e.g. `/multi-agent:help` body, `/multi-agent:status` table headers, summary blocks at the end of a run). Independent of `promptLanguage` so a user can have Turkish pickers but English summaries (or vice versa). External payloads (commits, PR/Jira/wiki) and skill-picker / confirmation / error UI exposed by the CLI host stay English regardless of this setting. Set via `/multi-agent:language output <en|tr>` or answered on first run by `/multi-agent:setup`."
        },
        "progressVerbosity": {
          "type": "string",
          "enum": ["quiet", "normal", "verbose"],
          "default": "normal",
          "description": "Verbosity of the live progress-line contract (`refs/progress-contract.md`). `quiet` shows only phase banners and user prompts (CI-friendly). `normal` shows banners + the canonical action set per phase - recommended for interactive use. `verbose` shows sub-agent internal actions, file-level reads, schema validations, retry internals - forced automatically when `mode: autopilot` is set in agent state so the run remains audit-grade when the user isn't watching live."
        },
        "triageCrossCheck": {
          "type": "object",
          "additionalProperties": false,
          "description": "Optional second-opinion check on Phase 4 triage decisions. When enabled, a configurable percentage of triage outputs are re-classified by Sonnet and the diff vs. Opus's verdict is logged for audit. Mitigates the single-point-of-failure risk that Opus triage hallucinates or drifts. Adds latency + cost; default off.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch."
            },
            "samplePct": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10,
              "description": "Percentage of triage runs to spot-check (1-100). 10 = roughly 1 in 10."
            },
            "model": {
              "type": "string",
              "enum": ["sonnet"],
              "default": "sonnet",
              "description": "Cross-check model. Locked to Sonnet - Opus would just agree with Opus."
            },
            "blockOnDisagreement": {
              "type": "boolean",
              "default": false,
              "description": "If true, a cross-check disagreement pauses Phase 5/6 for human review. If false (default), the disagreement is logged to metrics.jsonl as `triage.cross_check_diff` and the pipeline proceeds with Opus's original verdict."
            }
          }
        },
        "testBaseline": {
          "type": "object",
          "additionalProperties": false,
          "description": "Phase 0 test baseline. When enabled, Phase 0 runs the SAME test command Phase 4 Gate 3 uses and records which tests were already failing before this run touched anything, so Phase 4 stops attributing an inherited red suite to the current work. Three outcomes are stored, never two: green, red (with the failing set when it can be parsed, otherwise the log path alone), or unknown when the command is absent or the time cap was hit. Off by default because on iOS the run costs a full xcodebuild test before any work starts. Pattern source: obra/superpowers using-git-worktrees Step 3 'Verify Clean Baseline', extended from ask-the-user to a stored set Phase 4 can subtract.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. Off by default - a baseline run costs one full test suite before Phase 3 starts."
            },
            "timeoutSeconds": {
              "type": "integer",
              "minimum": 30,
              "maximum": 3600,
              "default": 600,
              "description": "Hard cap on the baseline run. Exceeding it records status unknown rather than a partial failing set: a truncated suite would look like passing tests that never ran."
            }
          }
        },
        "reviewDisagreementRound": {
          "type": "boolean",
          "default": false,
          "description": "v6.1.0+ - Phase 4 Step 2.5 rebuttal round. When reviewers disagree (mixed blocker/approved verdict), each reviewer is re-prompted with the others' opposing arguments for one additional round before triage. Lifts signal quality on ambiguous findings at ~1\u00d7 Step 2 token cost. Off by default - flip for security-critical or release-branch reviews."
        },
        "analysisProfiles": {
          "type": "array",
          "description": "v16.6+ - which analysis standards the /multi-agent:analysis Step 1b picker offers (Locked 32). Listing one value auto-resolves the step instead of asking a question whose answer is already settled. Omitted means both are offered.",
          "items": {
            "type": "string",
            "enum": ["global", "corporate"]
          },
          "default": ["global", "corporate"],
          "uniqueItems": true,
          "minItems": 1
        },
        "analysisProfile": {
          "type": "object",
          "additionalProperties": false,
          "description": "v16.6+ - per-profile deployment bindings. These are site configuration, not part of the shipped template: an unconfigured run still renders the full document and asks for its destination at Phase 3.5 like any other run. Keys are deliberately generic so no organisation's space, page or tooling names live in the repo.",
          "properties": {
            "corporate": {
              "type": "object",
              "additionalProperties": false,
              "description": "Bindings for the corporate profile: where its documents are published and which house terms they use.",
              "properties": {
                "confluenceSpaceKey": {
                  "type": "string",
                  "description": "Space the analysis page is created in when the Phase 3.5 picker chooses Confluence."
                },
                "confluenceParentPageId": {
                  "type": "string",
                  "description": "Parent page the analysis is filed under."
                },
                "titleFormat": {
                  "type": "string",
                  "description": "Page-title pattern, e.g. \"{prefix}{module}_{flow}_{suffix}\". Placeholders are resolved at emit time."
                },
                "titlePrefix": {
                  "type": "string",
                  "description": "Prefix that marks pipeline-authored pages so they stay distinguishable from hand-written ones."
                }
              }
            }
          }
        },
        "updateCheck": {
          "type": "object",
          "additionalProperties": false,
          "description": "v10.9+ - Phase 0 Step 0.6 version check. Once per ttlHours window, a bounded (3s) registry read compares the installed version against dist-tags.latest and dist-tags.required. Newer version found: interactive modes ask 'Update now / Continue' (yes runs the /multi-agent:update flow, then the run continues); autopilot logs one line and never asks; offline/failed checks are silent and never block. v15.14+ adds the required floor: when the installed version is BELOW dist-tags.required the run halts until /multi-agent:update ran - that branch is governed by no key here (enabled:false silences the advisory nag only), its single override is the env var MULTI_AGENT_ALLOW_OUTDATED=1, and it fails open on every undeterminable answer.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch for the ADVISORY 'update available' prompt. On by default - the cost is at most one 3s-bounded curl per ttlHours. Does not disable the required-version floor; see the object description."
            },
            "ttlHours": {
              "type": "integer",
              "minimum": 1,
              "maximum": 168,
              "default": 24,
              "description": "Cache window for the registry read."
            },
            "autoUpdate": {
              "type": "boolean",
              "default": true,
              "description": "Run the update flow automatically before the run starts, in interactive modes and autopilot alike, instead of asking. ON by default since v16.5.0: the alternative was telling every user to run a command, and the Supported Version Gate already halts an install below the required floor - so the real choice was never 'update or not', it was 'update, or stop and be told to update'. Set false to be asked once per ttlHours instead; updateCheck.enabled: false silences the check entirely (it does not disable the required floor). Already-loaded phase docs finish the current run on the old version either way; the update takes full effect on the next run."
            }
          }
        },
        "verifyByTest": {
          "type": "object",
          "additionalProperties": false,
          "description": "v10.8+ - Phase 4 Step 3.7 verify-by-test. When enabled, accepted BLOCKING findings are empirically validated before the Phase 3 rework loop: one verifier agent writes a minimal repro test per finding and runs only that test. Confirmed findings hand their failing test to Phase 3 as the RED step; non-reproducible findings are downgraded to deferred under evidence-gate. Only blocking findings are ever verified (fixed behavior, not a knob). Adds one model call plus up to maxFindings single-test runs per iteration with accepted blockers; default off. Flip on for security-critical work, release branches, or repos with noisy reviewers. Full spec: refs/features/verify-by-test.md.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch."
            },
            "maxFindings": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 3,
              "description": "Max accepted blocking findings verified per review iteration. Findings beyond the cap keep their judgment-only verdict."
            },
            "model": {
              "type": "string",
              "enum": ["sonnet", "opus"],
              "default": "sonnet",
              "description": "Verifier agent model. Writing a minimal repro test is mechanical work; Sonnet is the cost-sane default."
            },
            "stepTimeoutSec": {
              "type": "integer",
              "minimum": 60,
              "maximum": 1800,
              "default": 600,
              "description": "Wall-clock budget for the whole Step 3.7 pass. On breach, remaining findings keep judgment-only verdicts and the pipeline proceeds (never blocks)."
            }
          }
        },
        "review": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Standalone /multi-agent:review knobs. Only affects the post-pr-review.sh path (PR-mode comment posting); does not change Phase 4 reviewer dispatch inside the full pipeline.",
          "properties": {
            "dedupeInlineComments": {
              "type": "boolean",
              "default": true,
              "description": "Deduplicated PR review comments: before posting an inline comment, scan existing PR comments for a stable fingerprint marker (sha-16 of path|line|issue). If a comment with the same marker exists, skip  -  preserves audit trail without flooding the PR on re-runs. Provider-agnostic: GitHub /pulls/{n}/comments + /issues/{n}/comments and Bitbucket Server /pull-requests/{id}/activities?fromType=COMMENT are both checked. Set to false to restore the pre-v8.6 behavior (every run posts fresh comments, original spec)."
            }
          }
        },
        "shadowGit": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Cline-style per-tool-call shadow Git checkpoints. A separate git repo under ~/.claude/state/shadow-git/<task-id>/.git/ snapshots the worktree at each Phase 3 step boundary (per Plan Todo) and optionally after each Edit/Write/MultiEdit/Bash mutation. The shadow lives outside the project's real .git so semantic commits stay clean. Pattern source: Cline checkpoints (https://docs.cline.bot/features/checkpoints). Off by default  -  adds a per-step git commit on top of the existing TDD loop; flip on for risky refactors where sub-phase rollback is worth ~50ms per snapshot.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch."
            },
            "mode": {
              "type": "string",
              "enum": ["per-tool-call", "per-todo-step", "off"],
              "default": "per-todo-step",
              "description": "Snapshot frequency. per-tool-call snapshots after each Edit/Write/Bash mutation; per-todo-step snapshots once per plan-todos.sh step boundary (cheaper, recommended). off is equivalent to enabled=false."
            },
            "pruneAfterDays": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 14,
              "description": "Shadow snapshots older than N days are garbage-collected on the next /multi-agent:purge or by an explicit shadow-git.sh prune --older-than-days call."
            }
          }
        },
        "planTodos": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Plan-as-live-Todo-list. Phase 2 emits agent-state.plan.todos[] conforming to pipeline/schemas/plan-todos.schema.json; Phase 3 iterates step-by-step via pipeline/lib/plan-todos.sh next/start/complete; Phase 7 renders the rollup into agent-log.md and the PR body. The plan is broken into a live, structured Todo list. Off by default  -  opt in to add status-transition writes per Phase 3 step in exchange for sub-step visibility + per-step notes.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. When true, Phase 2 Step 4.5 emits plan.todos[] and Phase 3 / 7 read from it."
            }
          }
        },
        "clarifyAmbiguous": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Phase 0 Step 9 clarifying-question loop. Before Phase 1 starts, a cheap Haiku classifier scores task ambiguity (0-10) and emits up to N questions if score < threshold. Cost: ~$0.0025 per Haiku call. Off by default  -  flip on for teams burned by ambiguity-driven rework or when working on cross-team issues where the spec lives in someone else's head.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. When true, Phase 0 dispatches task-clarifier after maturity check and before Phase 1."
            },
            "model": {
              "type": "string",
              "enum": ["haiku", "sonnet"],
              "default": "haiku",
              "description": "Classifier model. Haiku is the cost default; Sonnet override for regulated domains where ambiguity nuance matters."
            },
            "minScoreToProceed": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10,
              "default": 6,
              "description": "Clarity threshold. Score \u2265 threshold \u2192 proceed silently. Below \u2192 questions fire. 6 is the borderline 'the what is clear but the how is fuzzy' line."
            },
            "maxQuestions": {
              "type": "integer",
              "minimum": 1,
              "maximum": 5,
              "default": 3,
              "description": "Hard cap on questions per round. Clarifier picks the highest-leverage N even if more ambiguity exists."
            },
            "autopilotMode": {
              "type": "string",
              "enum": ["skip", "log", "abort"],
              "default": "log",
              "description": "What to do when stopAndAsk fires in autopilot. skip = drop questions and proceed (most aggressive); log = append to agent-log.md and proceed (default, preserves signal); abort = pause Phase 0 and require multi-agent:resume."
            }
          }
        },
        "intentGuard": {
          "type": "object",
          "additionalProperties": false,
          "description": "v9.3+  -  Phase 0 conceptual-vs-edit guard for free-text input. A deterministic local classifier (pipeline/lib/classify-intent.sh, no model call) detects when the input is a question rather than a task and answers it in place instead of creating a branch/worktree. Targets the most-cited daily annoyance with coding agents (the agent edits code when asked a conceptual question). On by default because the check is free and only fires on free-text (Jira/issue inputs are always explicit tasks).",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "When true, free-text classified as a question is answered in chat and the dev chain is not started. Set false to always treat free-text as a task."
            }
          }
        },
        "costBudget": {
          "type": "object",
          "additionalProperties": false,
          "description": "v9.2+  -  Proactive per-task cost ceiling. Complements the end-of-run cost summary: pipeline/scripts/cost-budget-check.mjs reads the phase-tracker token accumulators after each phase, prices them via cost-table.json, and warns or halts before spend runs away. Top user-cited reason to abandon an AI coding tool is opaque/spiralling cost; this surfaces it live. Enabled by default in warn mode (v10.1+); set enabled:false to opt out, or onExceed:halt to make the ceiling blocking.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch. Default true (warn mode) so runaway cost is visible live. When off, cost-budget-check.mjs exits 0 silently and the pipeline behaves exactly as before."
            },
            "maxUsd": {
              "type": "number",
              "minimum": 0,
              "default": 5.0,
              "description": "Hard ceiling on estimated USD spend for a single task across all phases. Exceeding it returns exit 11."
            },
            "warnPct": {
              "type": "integer",
              "minimum": 1,
              "maximum": 99,
              "default": 80,
              "description": "Emit a warning (exit 10) once estimated spend reaches this percentage of maxUsd, so the user can intervene before the ceiling."
            },
            "onExceed": {
              "type": "string",
              "enum": ["warn", "halt"],
              "default": "warn",
              "description": "warn = surface the overage and continue (autopilot keeps going). halt = pause the run and require multi-agent:resume once the user accepts the cost. Interactive runs always surface the line regardless."
            },
            "pricingModel": {
              "type": "string",
              "enum": ["fable", "opus", "sonnet", "haiku"],
              "default": "fable",
              "description": "Which cost-table.json rate to price accumulated tokens at. Defaults to fable (the top tier since v10.6.0) for a deliberately conservative (upper-bound) estimate, so the ceiling trips early rather than late."
            }
          }
        },
        "usageLog": {
          "type": "object",
          "additionalProperties": false,
          "description": "Optional, opt-in per-run operational reporting via pipeline/scripts/usage-report.mjs. Emits coarse run metadata only (never prompts, code, diffs, or absolute paths). The emitter no-ops unless enabled is true AND a token resolves (from token below or env MULTI_AGENT_USAGE_TOKEN). Since v15.8.0 /multi-agent:update self-registers a per-machine write-only token and enables this automatically; optOut=true blocks that permanently.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch, on by default. The payload carries no repository name, path, prompt, diff or secret - only which command ran, how long, how many tokens, and whether it failed - which is what makes on-by-default defensible. Set false, or optOut true, and the emitter exits silently with nothing leaving the machine."
            },
            "optOut": {
              "type": "boolean",
              "default": false,
              "description": "Hard opt-out. When true, /multi-agent:update neither self-registers a token nor auto-enables reporting; enabled stays false until the user clears this."
            },
            "endpoint": {
              "type": "string",
              "description": "Ingest URL that receives the run record. The write-only token rides in the X-Usage-Token header, so the emitter sends ONLY over https (or http on 127.0.0.1/localhost for local dev); any other http endpoint is refused before the token is read."
            },
            "token": {
              "type": "string",
              "description": "Shared ingest token the receiving endpoint validates. May be left empty here and supplied via env MULTI_AGENT_USAGE_TOKEN instead. Local prefs only, never synced."
            }
          }
        },
        "analysisPhase": {
          "type": "object",
          "additionalProperties": false,
          "description": "Analysis-document behaviour, shared by the analysis mode and the full pipeline's Phase 1. Phase 2 and Phase 3 pre-flights refuse to run without the document, so these settings decide whether that document is produced, reused from cache, or recorded as not-applicable.",
          "properties": {
            "mode": {
              "type": "string",
              "enum": ["auto", "full", "lite"],
              "default": "auto",
              "description": "Section depth. auto picks lite for a bugfix or chore with no Figma reference and full otherwise; full and lite pin it. Layer selection (Analysis / Technical / Development) is a separate axis asked at intake."
            },
            "forceFull": {
              "type": "boolean",
              "default": false,
              "description": "Produce the document even for a task the when-table would skip (a bugfix or chore with no design reference). Use it when a small change still needs a written spec for review."
            },
            "commitDoc": {
              "type": "boolean",
              "default": true,
              "description": "Full pipeline only: whether the rendered document rides along with the work commit. Default true, so whoever reviews the PR sees the spec beside the code and the next run can reuse the evidence digest. When false the file stays in the worktree and is excluded from the commit."
            }
          }
        },
        "analyst": {
          "type": "object",
          "additionalProperties": false,
          "description": "Outside-fact sources for analysis work, served by the always-on ai-analyst-toolkit plugin. Two tiers, kept apart deliberately: evidence has a citable identity and may feed a specification body, signal is advisory and may only reach a risks section. Every source is best-effort  -  one that cannot be reached marks its row 'could not query' and the run continues.",
          "properties": {
            "evidence": {
              "type": "array",
              "items": { "type": "string", "enum": ["github", "registry"] },
              "default": ["github", "registry"],
              "description": "Citable sources. github reads issues, PRs and releases through the already-authenticated gh CLI; registry reads npm, PyPI, Maven Central and SPM over keyless HTTP."
            },
            "signals": {
              "type": "array",
              "items": { "type": "string", "enum": ["stackoverflow", "hackernews"] },
              "default": ["stackoverflow", "hackernews"],
              "description": "Advisory sources reachable without a key. Hacker News search goes through hn.algolia.com because the official Firebase API has no search endpoint."
            },
            "webSignals": {
              "type": "boolean",
              "default": false,
              "description": "Reddit and X, which have no usable free API left and are therefore reachable only through the host CLI's own web search. Off by default, best-effort when on, and exempt from cross-CLI parity because web search is not guaranteed on every host. Results are excluded from the Phase 1 evidence digest: they change by the hour, and including them would invalidate the cache on every run."
            }
          }
        },
        "reviewWatch": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Auto-review incoming PRs via gh CLI polling. Auto-triggers /multi-agent:review on PRs the user did NOT author. Disabled by default. Configure repos via .repos[] then either run pipeline/lib/review-watch.sh --watch as a background process or schedule it via cron.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch."
            },
            "repos": {
              "type": "array",
              "items": {
                "type": "string",
                "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"
              },
              "default": [],
              "description": "owner/repo entries to watch. The watcher only considers PRs not authored by the current gh user."
            },
            "intervalSeconds": {
              "type": "integer",
              "minimum": 30,
              "maximum": 86400,
              "default": 300,
              "description": "Polling interval for --watch loop. Clamped to \u226530s to stay polite with GitHub rate limits."
            },
            "labelFilter": {
              "type": "string",
              "description": "Optional GitHub label. When set, only PRs carrying this label are considered."
            }
          }
        },
        "repoMap": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+  -  Aider-style token-budgeted repo map injection. When enabled, the orchestrator runs pipeline/scripts/repo-map.mjs BEFORE Phase 1 (Analysis) and BEFORE Phase 4 (Review) and injects the resulting markdown as ${REPO_MAP} into the reviewer/analyser prompts. Deterministic  -  no embeddings, no network. Pattern source: https://aider.chat/docs/repomap.html. Off by default (introduces a ~150-300ms repo scan + repo-shaped prompt growth); flip on to lower per-call token cost on follow-up exploration phases.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. When true, Phase 1 and Phase 4 receive ${REPO_MAP} as additional context. When false, both phases run with only the diff + their normal prompt shell."
            },
            "tokenBudget": {
              "type": "integer",
              "minimum": 200,
              "maximum": 6000,
              "default": 1500,
              "description": "Soft token cap on the rendered map. The renderer fills greedily until the budget is hit, then truncates with a footer note. 1500 tokens ~ top 15-25 files with their top declarations."
            },
            "topFiles": {
              "type": "integer",
              "minimum": 5,
              "maximum": 100,
              "default": 25,
              "description": "Maximum number of ranked files to consider for inclusion. The token budget further trims this list  -  topFiles is an upper bound, not a guarantee."
            },
            "include": {
              "type": "string",
              "description": "Comma-separated path substrings  -  files NOT containing any of these are skipped. Empty/unset = include all supported sources. Use to scope the map to a sub-tree on monorepos (e.g. 'Sources/,Tests/')."
            },
            "exclude": {
              "type": "string",
              "description": "Comma-separated path substrings  -  files containing ANY of these are skipped. Stacks with their own caches (e.g. 'Generated/,.pb.swift,_mocks.kt') benefit from this."
            }
          }
        },
        "devCritic": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.6+ - Phase 3.5 evaluator-optimizer. After the Dev generator's last edit and BEFORE Phase 4 reviewers, dispatch agents/dev-critic.md (Sonnet by default) to run deterministic gates (build/lint/test/secrets) + the platform checklist (rules/*.md). Max 2 critic iterations, then escalate. Catches gate failures and checklist violations that would otherwise burn 2-3 Phase 4 reviewer calls + Opus triage. Off by default - introduces 1\u00d7 Sonnet call per Dev iteration; flip on for feature work, security-touching paths, or multi-file refactors. Source: Anthropic 'Building Effective Agents' (Dec 2024) evaluator-optimizer pattern.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. When true, Phase 3 dispatches dev-critic after the generator's last edit; when false, Phase 3 hands off directly to Phase 4."
            },
            "maxIterations": {
              "type": "integer",
              "minimum": 1,
              "maximum": 3,
              "default": 2,
              "description": "Critic loop cap. Round 1 = full pass. Round 2 = re-check round 1 failures only. Round 3 NOT recommended - escalate to user (interactive) or proceed with logged failure (autopilot)."
            },
            "model": {
              "type": "string",
              "enum": ["sonnet", "opus", "haiku"],
              "default": "sonnet",
              "description": "Critic model. Sonnet is the cost/quality default. Opus override only when diff > 500 LOC or touches security-critical paths. Haiku is too brittle for checklist reasoning - use only for trivial verification tasks."
            },
            "minDiffLoc": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "description": "Skip dev-critic when diff LOC is below this threshold (avoid the overhead on trivial typo/copy fixes). 0 = always run when enabled. 200 = a common 'feature work' threshold."
            }
          }
        },
        "diffRiskAdvisory": {
          "type": "boolean",
          "default": true,
          "description": "v8.3+ - Phase 4 Step 1.75 advisory diff risk scoring. When enabled, `pipeline/scripts/diff-risk-score.mjs` runs before reviewer dispatch and the top-N risk-ranked files are injected into each reviewer's prompt as a priority hint (security paths, public API surfaces, untested source changes, schema migrations). Heuristic, deterministic, no LLM cost. Default ON - the run is sub-second and never gates the pipeline. Flip to false to skip the script and the prompt injection entirely."
        },
        "reviewScopeGate": {
          "type": "boolean",
          "default": true,
          "description": "v12.8+ - Phase 4 Step 1.77 reviewer-scope gate. Decides the reviewer count from the deterministic diff-risk report via `pipeline/scripts/review-scope.mjs`: a diff under 20 lines of churn with max_score < 3.0 and no security_path / migration / public_api / no_test_change / test_lines_removed signal runs ONE reviewer instead of the full CLI-aware set (2 on Claude Code, 3 on Copilot CLI). No LLM. Fails safe in one direction only - any error, empty report or validator rejection resolves to the full set, because skipping a reviewer trades coverage for cost. Set false to force the full set on every diff.",
          "$comment": "Shipped inert for a release: the script existed, was unit- and smoke-tested, and no phase doc referenced it, so every diff paid for the full reviewer set. Wired in Step 1.77; smoke-gate-wiring.sh keeps it reachable."
        },
        "priorArtEnrichment": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.3+  -  Per-repo triage memory layer. When enabled, Phase 7 ingests every triage output's accepted/deferred/rejected rows into ~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl (idempotent), and Phase 4 triage queries the corpus for similar past findings to inject as context. Per-repo isolation  -  never cross-leaks between projects.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch for the lookup side. When false, Phase 4 triage runs without prior-art injection. Ingest still runs unless ingestOnComplete is also flipped."
            },
            "ingestOnComplete": {
              "type": "boolean",
              "default": true,
              "description": "When false, Phase 7 skips the corpus append. Use to keep an existing corpus frozen while still benefiting from lookups against it."
            },
            "topN": {
              "type": "integer",
              "minimum": 1,
              "maximum": 10,
              "default": 3,
              "description": "Number of prior-art entries to attach per current finding."
            }
          },
          "default": {
            "enabled": true,
            "ingestOnComplete": true,
            "topN": 3
          }
        },
        "learningsLedger": {
          "type": "object",
          "additionalProperties": false,
          "description": "v9.3+  -  Per-repo persistent LEARNINGS ledger (pipeline/scripts/learnings-ledger.mjs), stored next to triage-corpus at ~/.claude/memory/multi-agent/<repo-slug>/learnings-ledger.jsonl. Holds durable architectural facts, conventions, and explicitly rejected review preferences. A compact brief is injected into Phase 1 analysis and Phase 4 triage so agents stop re-discovering structure and reviewers stop re-flagging rejected feedback (the most-cited cold-boot-amnesia complaint). Phase 7 distills each run's rejected findings into the ledger. Per-repo isolated; never cross-leaks.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch. When false, no brief is injected and Phase 7 does not append to the ledger."
            },
            "injectIntoAnalysis": {
              "type": "boolean",
              "default": true,
              "description": "Inject the durable-knowledge brief into Phase 1 analysis context."
            },
            "injectIntoTriage": {
              "type": "boolean",
              "default": true,
              "description": "Inject the rejected-preference brief into Phase 4 triage so known-rejected suggestions are not re-accepted."
            },
            "maxBriefEntries": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20,
              "description": "Cap on entries in an injected brief (most-recent first)."
            }
          },
          "default": {
            "enabled": true,
            "injectIntoAnalysis": true,
            "injectIntoTriage": true,
            "maxBriefEntries": 20
          }
        },
        "memoryRecall": {
          "type": "object",
          "additionalProperties": false,
          "description": "v15.10+  -  How the per-repo memory stores decide WHICH entries to inject. Ranking is BM25 over the stored text (identifier-aware, IDF-weighted) fused with recency by Reciprocal Rank Fusion, shared by triage-memory.mjs and learnings-ledger.mjs via pipeline/scripts/_retrieval.mjs. Zero dependencies: no embedding service, no vector store, no extra model call. Before this, the triage corpus scored a raw token overlap and the ledger brief replayed the newest N entries, both of which stop discriminating once a repo has a few hundred rows.",
          "properties": {
            "strategy": {
              "type": "string",
              "enum": ["bm25", "legacy"],
              "default": "bm25",
              "description": "bm25 = relevance-ranked recall. legacy = the pre-ranking behaviour (token overlap for the triage corpus, newest-first for the ledger brief), kept as the one-flag rollback if a ranking change ever makes a run worse."
            },
            "maxResults": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 5,
              "description": "Default hit cap for a recall query when the caller passes no --top."
            },
            "maxChars": {
              "type": "integer",
              "minimum": 0,
              "default": 0,
              "description": "Character budget for one injected memory block, trimmed on whole lines. 0 disables the cap."
            },
            "halfLifeDays": {
              "type": "integer",
              "minimum": 1,
              "maximum": 3650,
              "default": 30,
              "description": "Recency half-life. Recency only separates entries of comparable relevance; it never promotes an unrelated entry."
            }
          },
          "default": {
            "strategy": "bm25",
            "maxResults": 5,
            "maxChars": 0,
            "halfLifeDays": 30
          }
        },
        "contextOffload": {
          "type": "object",
          "additionalProperties": false,
          "description": "v15.10+  -  Park bulky tool payloads (build logs, full diffs, test output) under <worktree>/.multi-agent/refs/<node_id>.md and put a stub in context instead: a header line, the tail, and the node id that fetches the rest. Applied by pipeline/scripts/offload-ref.sh as a pipe filter, so a phase that pipes through it behaves identically when this is off. Evidence gates keep reading the complete file - only what reaches the model shrinks.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": false,
              "description": "Master switch. Off by default: it changes what a phase prompt contains, which is a deliberate opt-in."
            },
            "minLines": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100000,
              "default": 40,
              "description": "Payloads shorter than this pass through untouched - a round trip through disk costs more than the lines it would save."
            },
            "tailLines": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000,
              "default": 20,
              "description": "Lines of the payload kept inline. For a failing build the error is at the end, which is why the tail rather than the head is kept."
            }
          },
          "default": {
            "enabled": false,
            "minLines": 40,
            "tailLines": 20
          }
        },
        "testGap": {
          "type": "object",
          "additionalProperties": false,
          "description": "v8.3+ - Phase 5 Step 0 advisory test-gap detector. Walks the diff for newly added public symbols and reports those without a paired test (or test method covering them). Heuristic, deterministic, no LLM cost.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Master switch. Default ON - the scan is sub-second."
            },
            "scanTree": {
              "type": "boolean",
              "default": false,
              "description": "When true, also scan the repo tree (not just the diff) for existing tests that already mention the symbol - catches gaps that are technically covered by older tests outside the current diff. Slower but more accurate. Off by default - diff-only scan is sufficient for the common case."
            },
            "blockingThreshold": {
              "type": ["integer", "null"],
              "minimum": 1,
              "default": null,
              "description": "If set, gaps where `important + blocking` count exceeds the threshold are treated as a Phase 4 rework finding (loops back to Phase 3). null = advisory only - Phase 5 prints the report but never gates."
            },
            "promoteSeverity": {
              "type": "boolean",
              "default": false,
              "description": "When true, the scan runs with --severity-promote, forcing every gap to 'important'. Useful for release branches and audit mode."
            }
          },
          "default": {
            "enabled": true,
            "scanTree": false,
            "blockingThreshold": null,
            "promoteSeverity": false
          }
        },
        "perRepoMemory": {
          "type": "boolean",
          "default": false,
          "description": "v6.2.0+ - Per-repo file-system memory layer. When enabled, Phase 0 reads `$PROJECT_ROOT/.multi-agent/memory/MEMORY.md` (if present) and injects it into the pipeline context, and Phase 7 dispatches a scoped synthesis subagent that may write new memory files to the same directory. Categories: user / feedback / project / reference (same taxonomy as the conversation-level auto-memory). Memory is local to each repo - never synced to remote, never committed to git (pipeline installer adds `.multi-agent/memory/` to `.gitignore`). Off by default - flip when working on a repo long enough that accumulated project context starts paying for itself."
        },
        "autopilotSafetyGate": {
          "type": "boolean",
          "default": true,
          "description": "v7.0.0+ - Phase 2 autopilot safety classifier. Before autopilot mode consumes the user's approval skip, run `classify-plan-safety.mjs` over the approved plan. If the heuristic score \u2265 50 (e.g. >15 files touched, or security-path touch, or delete-without-test, or schema migration) inject a ONE-TIME pause asking for explicit manual approval - even in autopilot. Default ON because the risk of skipping this gate is asymmetric: a pause on a high-blast-radius plan costs seconds; a silent auto-merge of a bad one costs hours of rollback. Flip to `false` only for tightly-scoped autopilot workflows (e.g. batch figma component iteration) where the task class is known-safe."
        },
        "dynamicSkillLoading": {
          "type": "boolean",
          "default": false,
          "description": "v7.0.0+ - Dynamic trigger-based skill loading. When enabled, the orchestrator reads `~/.claude/skills/.skills-index.json` after Phase 1 analysis and injects only the top-N matched skills into subagent prompts (via `match-skills.mjs`), instead of relying on eager-loaded SKILL.md bodies. Large token win on small tasks; no visible behavior change on big ones where many skills match. The index ships with every install - full and `--index-only` alike - so this is safe to flip on any installed tree. It was not always: until v13.6 only `--index-only` copied the index, and `match-skills.mjs` resolved its default index path relative to a repo checkout, so enabling this on a normal install produced `cannot read index` and a silent fall back to eager loading. Off by default - flip to `true` to opt in; a future release may flip the default once enough telemetry confirms no regression."
        },
        "derivedSkillSources": {
          "type": "array",
          "description": "v11.3.0+. Skills/components in a project that were derived from an upstream marketplace plugin. `/multi-agent:refactor` Step 0b compares each entry's `derivedFromVersion` against the installed upstream version and reports drift (plan band D). Local-only: it may name private marketplaces, so it is never synced into the public repo. Empty array = the drift step is skipped.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["label", "localPath", "upstreamPlugin", "derivedFromVersion"],
            "properties": {
              "label": {
                "type": "string",
                "description": "Human name for this derivation, shown in the drift table."
              },
              "localPath": {
                "type": "string",
                "description": "Repo-relative directory holding our derived copy."
              },
              "upstreamMarketplace": {
                "type": "string",
                "description": "Installed marketplace name, resolved under ~/.claude/plugins/cache/<marketplace>/."
              },
              "upstreamPlugin": {
                "type": "string",
                "description": "Upstream plugin name inside that marketplace."
              },
              "upstreamSkills": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The upstream skill names we took."
              },
              "derivedFromVersion": {
                "type": "string",
                "description": "Upstream version we last synced from, read from the manifest named by `upstreamVersionSource`. Bumped after a drift port is applied."
              },
              "upstreamVersionSource": {
                "enum": ["marketplace.json", "plugin.json"],
                "default": "marketplace.json",
                "description": "Which upstream manifest carries the authoritative version. Defaults to marketplace.json because that is what a marketplace consumer resolves; some upstreams keep plugin.json deliberately unused, so reading it silently records a version nobody ships. Recording from the wrong manifest is how an entry here sat at 0.7.0 while the marketplace said 0.6.0."
              },
              "upstreamLocalClone": {
                "type": "string",
                "description": "Working copy of the upstream repository, checked before the plugin cache. The cache under ~/.claude/plugins/cache is only as fresh as the last `claude marketplace update`, so it is a mirror and never the authority. A private upstream may also be unreachable over the API from the active account, in which case this clone is the only source."
              },
              "upstreamRepoUrl": {
                "type": "string",
                "description": "Used when neither a local clone nor the installed marketplace is available (WebFetch / gh fallback)."
              },
              "driftAcknowledged": {
                "type": "string",
                "pattern": "^\\d+\\.\\d+\\.\\d+$",
                "description": "Upstream version whose drift is consciously accepted (port pending). The drift gate reports it but does not fail while upstream stays at this version; it re-fails the moment upstream moves past it."
              }
            }
          }
        },
        "devToolkit": {
          "type": "object",
          "additionalProperties": false,
          "description": "v12.6.0+. The companion MCP server that serves the pipeline's device and browser tools (multi-agent-toolkit-mcp). Two commands read this: `/multi-agent:refactor` Step 0c researches current MCP practice and audits that repo against it (plan band E), and `/multi-agent:sync` Step 3d ships it when it moved (gates, commit, publish). When absent, both commands fall back to auto-detecting the repo from the `mcpServers` registration in ~/.claude.json / ~/.claude/settings.json, and skip silently when that fails. Never a hardcoded path.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "default": true,
              "description": "Set false to skip both the refactor band-E research and the sync ship step."
            },
            "label": {
              "type": "string",
              "description": "Human name used in reports."
            },
            "localPath": {
              "type": "string",
              "description": "Working copy of the companion repo. `$HOME` and `~` are expanded. When omitted, resolved from the MCP registration (dirname of the stdio server's script)."
            },
            "mcpServerName": {
              "type": "string",
              "default": "multi-agent-toolkit",
              "description": "Registered MCP server name, used for the auto-detect fallback and for telling the user whether a restart or a reinstall is needed after a publish."
            },
            "packageName": {
              "type": "string",
              "description": "Published package name, e.g. @<scope>/<package>. Read from the repo's package.json when omitted."
            },
            "registry": {
              "type": "string",
              "enum": ["github-packages", "npmjs", "none"],
              "default": "github-packages",
              "description": "Publish target for Step 3d. `none` means commit + push only, never publish. The concrete URL comes from the repo's publishConfig; the token comes from the `npm` logical Keychain key through a throwaway --userconfig."
            },
            "repoUrl": {
              "type": "string",
              "description": "https remote, used in reports and when the working copy has to be cloned by the user."
            }
          }
        },
        "recentAccounts": {
          "type": "array",
          "description": "v7.3.0+. Last-used-first LRU of account ids (from account-resolver.sh). Used by autopilot to pick the most recent account when multiple are configured.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id"],
            "properties": {
              "id": {
                "type": "string",
                "description": "Short id from account-resolver, e.g. my-account"
              },
              "prefix": {
                "type": "string",
                "description": "Original keychain prefix, e.g. ${USER}"
              },
              "lastUsed": {
                "type": "string",
                "format": "date-time"
              },
              "count": {
                "type": "integer",
                "minimum": 0
              }
            }
          },
          "maxItems": 10
        },
        "accounts": {
          "type": "array",
          "description": "v7.4.0+. Per-account overrides - host, label, default platform. Resolved by account-resolver.sh by matching `id` (e.g. my-account). When unset, hosts fall back to global.hosts.{jira,bitbucket}.",
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": ["id"],
            "properties": {
              "id": {
                "type": "string",
                "description": "Short id matching account-resolver output (lowercase, dashes)."
              },
              "label": {
                "type": "string",
                "description": "Display label override; otherwise the resolver's auto-generated label is used."
              },
              "jiraHost": {
                "type": "string",
                "description": "Per-account Jira host override (no scheme). Wins over global.hosts.jira."
              },
              "bitbucketHost": {
                "type": "string",
                "description": "Per-account Bitbucket host override (no scheme). Wins over global.hosts.bitbucket."
              },
              "confluenceHost": {
                "type": "string"
              },
              "defaultProvider": {
                "type": "string",
                "enum": ["github", "bitbucket", "gitlab", "generic-git"]
              }
            }
          },
          "maxItems": 20
        },
        "skillConformance": {
          "type": "object",
          "additionalProperties": false,
          "description": "v14.0.0+ - Phase 4 Step 1.78 criteria resolution. The stage itself has no on/off key and neither does its exception-expiry check: a run that can switch off its own anti-reward-hacking control cannot be trusted to report a pass (same reasoning as Step 1.76).",
          "properties": {
            "blockOnCoverageGap": {
              "type": "boolean",
              "default": false,
              "description": "Whether a declared coverage gap halts the run. A gap means a language in the diff that no rule registry covers, and it is REPORTED either way - this key only decides whether it also stops the pipeline. Default false because only Swift plus the two store-compliance catalogs ship a scoped registry today, so defaulting true would block every Kotlin, Python, Node and Objective-C run on day one. Flip to true once your stack's registry exists and you want an uncovered file treated as a failure rather than a note."
            }
          }
        },
        "resumeLocal": {
          "type": "object",
          "additionalProperties": false,
          "description": "/multi-agent:resume-local (formerly :ship, :finish) - the tail that runs review + build/test + PR + report over work already on the branch.",
          "properties": {
            "autoFix": {
              "type": "boolean",
              "default": false,
              "description": "When true, resume-local auto-fixes triage-accepted blocking/important findings and re-reviews instead of asking. Equivalent to passing `autopilot` on every run. Default false: resume-local operates on work the user wrote by hand, so silently rewriting it is the surprising option."
            }
          }
        }
      }
    },
    "projects": {
      "type": "object",
      "description": "Per-project overrides. Key = project slug.",
      "patternProperties": {
        "^[a-z0-9][a-z0-9\\-_]*$": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "testPolicy": {
              "type": "string",
              "enum": ["tdd", "tests-after", "none"],
              "description": "How Phase 3 authors tests in this project: tdd (default; failing test first), tests-after (implementation first, tests authored at the end), none (no unit/UI test authoring; existing tests are kept and run). Asked once by Phase 0 when absent; autopilot defaults to tdd and notes it."
            },
            "defaultReviewers": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 40,
              "description": "Bitbucket/generic default reviewers (usernames). Real corporate reviewer groups run well past ten members."
            },
            "githubDefaultReviewers": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 10,
              "description": "GitHub fallback reviewers - @user or org/team slug. Used only when no CODEOWNERS / branch protection surfaces reviewers."
            },
            "defaultPrMode": {
              "type": "string",
              "enum": ["draft", "ready"],
              "description": "Last-used draft/ready choice, shown as the default on next prompt."
            },
            "baseBranch": {
              "type": "string",
              "description": "Project default base branch. Falls back to repo HEAD."
            },
            "confluenceUrls": {
              "type": "array",
              "items": {
                "type": "string",
                "format": "uri"
              },
              "maxItems": 5,
              "description": "Recent parent page URLs, most recent first."
            },
            "branches": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 10,
              "description": "Legacy field. Recent branches for this project (pre-v2.1.0). v2.1.0+ prefers global.recentBranches[projectPath]."
            },
            "lastIdentity": {
              "type": "integer",
              "minimum": 0,
              "description": "Legacy field. Index into global.gitIdentities (v2.0.0). v2.1.0+ uses platformIdentityRouting."
            },
            "jiraProjectKeys": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 10,
              "description": "Per-project Jira project keys (can override global.defaultJiraKey)."
            },
            "remoteType": {
              "type": "string",
              "enum": ["bitbucket", "github", "gitlab", "generic-git"],
              "description": "Hint used when repo URL doesn't make platform obvious."
            },
            "taskCount": {
              "type": "integer",
              "minimum": 0,
              "description": "How many times this project has been used."
            },
            "lastUsed": {
              "type": "string",
              "format": "date-time",
              "description": "Timestamp of last task start against this project."
            },
            "componentDevWorkflow": {
              "type": "boolean",
              "description": "Project follows the component (Configuration / View / Modifiers) development workflow, so Phase 3 dispatches the component skills instead of the standard TDD flow."
            },
            "figmaConfigPath": {
              "type": "string",
              "description": "Path to per-project figma-config.json (for Figma pipeline projects)."
            },
            "standardsFile": {
              "type": "string",
              "description": "Filename (or absolute/tilde path) of the canonical home-dir Standards reference for this project, used by /multi-agent:analysis Q5 auto-detect (e.g. ~/<project>-Standards.md)."
            },
            "editableRelatedRepos": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 20,
              "description": "Extra writable repos to offer in the dev-context picker beyond auto-detected submodules (iOS/Android/Backend)."
            },
            "frontendRepos": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 20,
              "description": "Frontend repos for this project, used by /multi-agent:analysis Step 4 (Frontend is rarely in the iOS/Android submodule tree)."
            },
            "frontendRoots": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "maxItems": 20,
              "description": "Whitelist source roots for the Frontend repo-evidence scan (overrides the default src/ app/ components/ features/ lib/)."
            },
            "appStoreConnect": {
              "type": "object",
              "description": "Per-project App Store Connect delivery settings. Per-project rather than global because a user can ship for more than one team.",
              "properties": {
                "providerPublicId": {
                  "type": ["string", "null"],
                  "description": "Provider (team) public ID, required by altool when the account belongs to several providers. Resolve once with ios_testflight_validate({list_providers:true}); without it altool fails opaquely."
                }
              }
            }
          }
        }
      }
    }
  }
}
