agents-united 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +507 -405
  2. package/dist/cli.js +1772 -629
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +10 -4
  5. package/registry/agents/orchestrator-business.md +21 -5
  6. package/registry/agents/orchestrator-design.md +21 -5
  7. package/registry/agents/orchestrator-digital-agency.md +9 -1
  8. package/registry/agents/orchestrator-engineering.md +12 -5
  9. package/registry/agents/orchestrator-marketing.md +14 -5
  10. package/registry/agents/orchestrator-research.md +14 -5
  11. package/registry/agents/orchestrator-security.md +15 -5
  12. package/registry/agents/orchestrator-system-architecture.md +14 -5
  13. package/registry/agents/orchestrator-universal.md +11 -2
  14. package/registry/agents/subagent-accessibility-lead.md +49 -3
  15. package/registry/agents/subagent-ai-model-architect.md +48 -2
  16. package/registry/agents/subagent-android-architect.md +53 -3
  17. package/registry/agents/subagent-appsec-penetration-tester.md +27 -1
  18. package/registry/agents/subagent-backend-architect.md +63 -232
  19. package/registry/agents/subagent-business-panel-experts.md +5 -1
  20. package/registry/agents/subagent-cloud-infrastructure-architect.md +23 -1
  21. package/registry/agents/subagent-cloud-security-architect.md +7 -1
  22. package/registry/agents/subagent-code-reviewer.md +31 -1
  23. package/registry/agents/subagent-compliance-grc-specialist.md +7 -2
  24. package/registry/agents/subagent-cross-platform-specialist.md +54 -3
  25. package/registry/agents/subagent-data-engineer.md +53 -3
  26. package/registry/agents/subagent-database-administrator.md +7 -1
  27. package/registry/agents/subagent-deep-research.md +5 -1
  28. package/registry/agents/subagent-design-ops-lead.md +5 -1
  29. package/registry/agents/subagent-design-researcher.md +5 -1
  30. package/registry/agents/subagent-design-systems-architect.md +5 -1
  31. package/registry/agents/subagent-designer-toolkit-expert.md +5 -1
  32. package/registry/agents/subagent-devops-engineer.md +68 -288
  33. package/registry/agents/subagent-distributed-systems-architect.md +54 -3
  34. package/registry/agents/subagent-e2e-tester.md +52 -3
  35. package/registry/agents/subagent-financial-analyst.md +5 -1
  36. package/registry/agents/subagent-finops-cost-engineer.md +5 -1
  37. package/registry/agents/subagent-frontend-architect.md +54 -225
  38. package/registry/agents/subagent-interaction-designer.md +5 -1
  39. package/registry/agents/subagent-ios-architect.md +53 -3
  40. package/registry/agents/subagent-legal-contract-analyst.md +5 -1
  41. package/registry/agents/subagent-lifecycle-email-specialist.md +5 -1
  42. package/registry/agents/subagent-literature-patent-analyst.md +5 -1
  43. package/registry/agents/subagent-market-intelligence-analyst.md +5 -1
  44. package/registry/agents/subagent-marketing-campaign-specialist.md +6 -2
  45. package/registry/agents/subagent-marketing-content-strategist.md +6 -2
  46. package/registry/agents/subagent-marketing-conversion-specialist.md +6 -2
  47. package/registry/agents/subagent-marketing-creative-designer.md +6 -2
  48. package/registry/agents/subagent-marketing-growth-strategist.md +6 -2
  49. package/registry/agents/subagent-ml-platform-engineer.md +48 -2
  50. package/registry/agents/subagent-operations-strategist.md +5 -1
  51. package/registry/agents/subagent-paid-acquisition-specialist.md +5 -1
  52. package/registry/agents/subagent-plg-strategist.md +5 -1
  53. package/registry/agents/subagent-prototype-tester.md +5 -1
  54. package/registry/agents/subagent-qa-automation-lead.md +35 -78
  55. package/registry/agents/subagent-repo-index.md +27 -1
  56. package/registry/agents/subagent-security-engineer.md +37 -1
  57. package/registry/agents/subagent-seo-specialist.md +6 -2
  58. package/registry/agents/subagent-socratic-mentor.md +5 -1
  59. package/registry/agents/subagent-statistical-analyst.md +5 -1
  60. package/registry/agents/subagent-sysops-sre-lead.md +7 -1
  61. package/registry/agents/subagent-system-architect.md +6 -1
  62. package/registry/agents/subagent-ui-designer.md +5 -1
  63. package/registry/agents/subagent-ux-strategist.md +5 -1
  64. package/registry/bundles.json +89 -17
  65. package/registry/core/orchestrator-engineering.core.md +1 -0
  66. package/registry/core/subagent-backend-architect.core.md +20 -22
  67. package/registry/core/subagent-code-reviewer.core.md +1 -0
  68. package/registry/core/subagent-frontend-architect.core.md +8 -6
  69. package/registry/core/subagent-repo-index.core.md +1 -0
  70. package/registry/skills/ai-prototype-refactoring/SKILL.md +24 -0
  71. package/registry/skills/ai-prototype-refactoring/references/backend-frontend-devops-exemplars.md +104 -0
  72. package/registry/skills/azure-infrastructure-bicep/SKILL.md +24 -0
  73. package/registry/skills/azure-infrastructure-bicep/references/backend-frontend-devops-exemplars.md +173 -0
  74. package/registry/skills/banner-design/SKILL.md +170 -0
  75. package/registry/skills/banner-design/references/banner-sizes-and-styles.md +66 -0
  76. package/registry/skills/brand-identity/SKILL.md +212 -0
  77. package/registry/skills/brand-identity/references/approval-checklist.md +17 -0
  78. package/registry/skills/brand-identity/references/asset-organization.md +30 -0
  79. package/registry/skills/brand-identity/references/brand-guideline-template.md +57 -0
  80. package/registry/skills/brand-identity/references/color-palette-management.md +37 -0
  81. package/registry/skills/brand-identity/references/consistency-checklist.md +26 -0
  82. package/registry/skills/brand-identity/references/logo-usage-rules.md +23 -0
  83. package/registry/skills/brand-identity/references/messaging-framework.md +22 -0
  84. package/registry/skills/brand-identity/references/typography-specifications.md +29 -0
  85. package/registry/skills/brand-identity/references/visual-identity.md +23 -0
  86. package/registry/skills/brand-identity/references/voice-framework.md +27 -0
  87. package/registry/skills/brand-identity/scripts/extract-colors.mjs +104 -0
  88. package/registry/skills/brand-identity/scripts/inject-brand-context.mjs +112 -0
  89. package/registry/skills/brand-identity/scripts/lib/confirm-gate.mjs +28 -0
  90. package/registry/skills/brand-identity/scripts/sync-brand-to-tokens.mjs +205 -0
  91. package/registry/skills/brand-identity/scripts/validate-asset.mjs +137 -0
  92. package/registry/skills/clickhouse-architecture-advisor/SKILL.md +202 -0
  93. package/registry/skills/codeql-scanning/LICENSE +427 -0
  94. package/registry/skills/codeql-scanning/NOTICE.md +31 -0
  95. package/registry/skills/codeql-scanning/SKILL.md +136 -0
  96. package/registry/skills/codeql-scanning/references/build-fixes.md +105 -0
  97. package/registry/skills/codeql-scanning/references/diagnostic-query-templates.md +325 -0
  98. package/registry/skills/codeql-scanning/references/extension-yaml-format.md +202 -0
  99. package/registry/skills/codeql-scanning/references/important-only-suite.md +119 -0
  100. package/registry/skills/codeql-scanning/references/language-details.md +220 -0
  101. package/registry/skills/codeql-scanning/references/macos-arm64e-workaround.md +201 -0
  102. package/registry/skills/codeql-scanning/references/performance-tuning.md +81 -0
  103. package/registry/skills/codeql-scanning/references/quality-assessment.md +197 -0
  104. package/registry/skills/codeql-scanning/references/ruleset-catalog.md +68 -0
  105. package/registry/skills/codeql-scanning/references/run-all-suite.md +106 -0
  106. package/registry/skills/codeql-scanning/references/sarif-processing.md +82 -0
  107. package/registry/skills/codeql-scanning/references/threat-models.md +36 -0
  108. package/registry/skills/codeql-scanning/references/upstream-method.md +288 -0
  109. package/registry/skills/codeql-scanning/references/workflow-build-database.md +338 -0
  110. package/registry/skills/codeql-scanning/references/workflow-create-data-extensions.md +275 -0
  111. package/registry/skills/codeql-scanning/references/workflow-run-analysis.md +327 -0
  112. package/registry/skills/codeql-scanning/scripts/build_log.py +91 -0
  113. package/registry/skills/codeql-scanning/scripts/check_db_quality.py +366 -0
  114. package/registry/skills/codeql-scanning/scripts/find_databases.py +90 -0
  115. package/registry/skills/codeql-scanning/scripts/generate_suite.py +101 -0
  116. package/registry/skills/codeql-scanning/scripts/verify_query_suite.py +86 -0
  117. package/registry/skills/edge-security-audit/SKILL.md +225 -0
  118. package/registry/skills/expo-cicd-workflows/SKILL.md +228 -0
  119. package/registry/skills/hf-managed-jobs/SKILL.md +198 -0
  120. package/registry/skills/hf-model-training/SKILL.md +204 -0
  121. package/registry/skills/mutation-testing/LICENSE +427 -0
  122. package/registry/skills/mutation-testing/NOTICE.md +31 -0
  123. package/registry/skills/mutation-testing/SKILL.md +122 -0
  124. package/registry/skills/mutation-testing/references/blockchain-patterns.md +103 -0
  125. package/registry/skills/mutation-testing/references/equivalent-mutants.md +101 -0
  126. package/registry/skills/mutation-testing/references/input-formats.md +84 -0
  127. package/registry/skills/mutation-testing/references/optimization-strategies.md +327 -0
  128. package/registry/skills/mutation-testing/references/report-template.md +140 -0
  129. package/registry/skills/mutation-testing/references/severity-classification.md +132 -0
  130. package/registry/skills/mutation-testing/references/upstream-method.md +105 -0
  131. package/registry/skills/mutation-testing/references/workflow-analyzing-results.md +123 -0
  132. package/registry/skills/mutation-testing/references/workflow-bug-hunter.md +139 -0
  133. package/registry/skills/mutation-testing/references/workflow-configuration.md +338 -0
  134. package/registry/skills/playwright-best-practices/SKILL.md +5 -0
  135. package/registry/skills/playwright-best-practices/references/conversion-funnel-test.md +79 -0
  136. package/registry/skills/postgres-best-practices/SKILL.md +205 -0
  137. package/registry/skills/property-based-testing/LICENSE +427 -0
  138. package/registry/skills/property-based-testing/NOTICE.md +31 -0
  139. package/registry/skills/property-based-testing/SKILL.md +119 -0
  140. package/registry/skills/property-based-testing/references/generating.md +82 -0
  141. package/registry/skills/property-based-testing/references/interpreting-failures.md +83 -0
  142. package/registry/skills/property-based-testing/references/libraries.md +80 -0
  143. package/registry/skills/property-based-testing/references/refactoring.md +123 -0
  144. package/registry/skills/property-based-testing/references/reviewing.md +81 -0
  145. package/registry/skills/property-based-testing/references/upstream-method.md +65 -0
  146. package/registry/skills/react-best-practices/SKILL.md +5 -0
  147. package/registry/skills/react-best-practices/references/nextjs-server-actions-and-isr.md +85 -0
  148. package/registry/skills/sarif-triage/LICENSE +427 -0
  149. package/registry/skills/sarif-triage/NOTICE.md +31 -0
  150. package/registry/skills/sarif-triage/SKILL.md +117 -0
  151. package/registry/skills/sarif-triage/references/jq-queries.md +210 -0
  152. package/registry/skills/sarif-triage/references/upstream-method.md +490 -0
  153. package/registry/skills/sarif-triage/scripts/sarif_helpers.py +400 -0
  154. package/registry/skills/security-best-practices/LICENSE +201 -0
  155. package/registry/skills/security-best-practices/NOTICE.md +30 -0
  156. package/registry/skills/security-best-practices/SKILL.md +116 -0
  157. package/registry/skills/security-best-practices/references/golang-general-backend-security.md +829 -0
  158. package/registry/skills/security-best-practices/references/javascript-express-web-server-security.md +1161 -0
  159. package/registry/skills/security-best-practices/references/javascript-general-web-frontend-security.md +750 -0
  160. package/registry/skills/security-best-practices/references/javascript-jquery-web-frontend-security.md +681 -0
  161. package/registry/skills/security-best-practices/references/javascript-typescript-nextjs-web-server-security.md +1147 -0
  162. package/registry/skills/security-best-practices/references/javascript-typescript-react-web-frontend-security.md +993 -0
  163. package/registry/skills/security-best-practices/references/javascript-typescript-vue-web-frontend-security.md +794 -0
  164. package/registry/skills/security-best-practices/references/python-django-web-server-security.md +885 -0
  165. package/registry/skills/security-best-practices/references/python-fastapi-web-server-security.md +1039 -0
  166. package/registry/skills/security-best-practices/references/python-flask-web-server-security.md +708 -0
  167. package/registry/skills/security-best-practices/references/upstream-method.md +86 -0
  168. package/registry/skills/security-diff-review/LICENSE +427 -0
  169. package/registry/skills/security-diff-review/NOTICE.md +31 -0
  170. package/registry/skills/security-diff-review/SKILL.md +125 -0
  171. package/registry/skills/security-diff-review/references/adversarial.md +206 -0
  172. package/registry/skills/security-diff-review/references/methodology.md +237 -0
  173. package/registry/skills/security-diff-review/references/patterns.md +303 -0
  174. package/registry/skills/security-diff-review/references/reporting.md +372 -0
  175. package/registry/skills/security-diff-review/references/upstream-method.md +222 -0
  176. package/registry/skills/semgrep-scanning/LICENSE +427 -0
  177. package/registry/skills/semgrep-scanning/NOTICE.md +31 -0
  178. package/registry/skills/semgrep-scanning/SKILL.md +139 -0
  179. package/registry/skills/semgrep-scanning/references/rulesets.md +165 -0
  180. package/registry/skills/semgrep-scanning/references/scan-modes.md +153 -0
  181. package/registry/skills/semgrep-scanning/references/upstream-method.md +262 -0
  182. package/registry/skills/semgrep-scanning/references/workflow-scan-workflow.md +467 -0
  183. package/registry/skills/semgrep-scanning/scripts/merge_sarif.py +339 -0
  184. package/registry/skills/semgrep-scanning/scripts/run_scans.py +482 -0
  185. package/registry/skills/sentry-incident-triage/SKILL.md +193 -0
  186. package/registry/skills/supabase-backend-architecture/SKILL.md +24 -0
  187. package/registry/skills/supabase-backend-architecture/references/backend-frontend-devops-exemplars.md +140 -0
  188. package/registry/skills/supply-chain-risk-audit/LICENSE +427 -0
  189. package/registry/skills/supply-chain-risk-audit/NOTICE.md +31 -0
  190. package/registry/skills/supply-chain-risk-audit/SKILL.md +119 -0
  191. package/registry/skills/supply-chain-risk-audit/references/upstream-method.md +123 -0
  192. package/registry/skills/supply-chain-risk-audit/scripts/collect.py +1568 -0
  193. package/registry/skills/supply-chain-risk-audit/scripts/model.py +402 -0
  194. package/registry/skills/supply-chain-risk-audit/scripts/pyproject.toml +20 -0
  195. package/registry/skills/supply-chain-risk-audit/scripts/render.py +809 -0
  196. package/registry/skills/supply-chain-risk-audit/scripts/sources.py +683 -0
  197. package/registry/skills/supply-chain-risk-audit/scripts/uv.lock +12 -0
  198. package/registry/skills/terraform-style-guide/LICENSE +373 -0
  199. package/registry/skills/terraform-style-guide/NOTICE.md +31 -0
  200. package/registry/skills/terraform-style-guide/SKILL.md +126 -0
  201. package/registry/skills/terraform-style-guide/references/security.md +167 -0
  202. package/registry/skills/terraform-style-guide/references/style-conventions.md +312 -0
  203. package/registry/skills/terraform-test-patterns/LICENSE +373 -0
  204. package/registry/skills/terraform-test-patterns/NOTICE.md +31 -0
  205. package/registry/skills/terraform-test-patterns/SKILL.md +133 -0
  206. package/registry/skills/terraform-test-patterns/references/ci-cd.md +83 -0
  207. package/registry/skills/terraform-test-patterns/references/examples.md +317 -0
  208. package/registry/skills/terraform-test-patterns/references/mock-providers.md +174 -0
  209. package/registry/skills/terraform-test-patterns/references/test-syntax.md +445 -0
  210. package/registry/skills/threat-modeling/LICENSE +201 -0
  211. package/registry/skills/threat-modeling/NOTICE.md +31 -0
  212. package/registry/skills/threat-modeling/SKILL.md +136 -0
  213. package/registry/skills/threat-modeling/references/prompt-template.md +258 -0
  214. package/registry/skills/threat-modeling/references/security-controls-and-assets.md +35 -0
  215. package/registry/skills/threat-modeling/references/upstream-method.md +81 -0
  216. package/registry/skills/turso-distributed-sqlite/SKILL.md +24 -0
  217. package/registry/skills/turso-distributed-sqlite/references/backend-frontend-devops-exemplars.md +92 -0
  218. package/registry/skills/ux-writing/SKILL.md +183 -0
  219. package/registry/skills/ux-writing/references/interface-patterns.md +41 -0
  220. package/registry/skills/ux-writing/references/localization-accessibility.md +31 -0
  221. package/registry/skills/ux-writing/references/research-measurement.md +27 -0
  222. package/registry/skills/ux-writing/references/source-runtime-workflow.md +31 -0
  223. package/registry/skills/ux-writing/references/voice-tone-terminology.md +26 -0
  224. package/registry/skills/variant-analysis/LICENSE +427 -0
  225. package/registry/skills/variant-analysis/NOTICE.md +31 -0
  226. package/registry/skills/variant-analysis/SKILL.md +119 -0
  227. package/registry/skills/variant-analysis/assets/codeql/cpp.ql +119 -0
  228. package/registry/skills/variant-analysis/assets/codeql/go.ql +69 -0
  229. package/registry/skills/variant-analysis/assets/codeql/java.ql +71 -0
  230. package/registry/skills/variant-analysis/assets/codeql/javascript.ql +63 -0
  231. package/registry/skills/variant-analysis/assets/codeql/python.ql +80 -0
  232. package/registry/skills/variant-analysis/assets/semgrep/cpp.yaml +98 -0
  233. package/registry/skills/variant-analysis/assets/semgrep/go.yaml +63 -0
  234. package/registry/skills/variant-analysis/assets/semgrep/java.yaml +61 -0
  235. package/registry/skills/variant-analysis/assets/semgrep/javascript.yaml +60 -0
  236. package/registry/skills/variant-analysis/assets/semgrep/python.yaml +72 -0
  237. package/registry/skills/variant-analysis/references/reporting.md +41 -0
  238. package/registry/skills/variant-analysis/references/root-cause.md +101 -0
  239. package/registry/skills/variant-analysis/references/searching.md +162 -0
  240. package/registry/skills/variant-analysis/references/triage.md +95 -0
  241. package/registry/skills/variant-analysis/references/upstream-method.md +74 -0
  242. package/registry/skills/variant-analysis/references/variant-report-template.md +78 -0
  243. package/registry/skills/vercel-deploy-best-practices/SKILL.md +24 -0
  244. package/registry/skills/vercel-deploy-best-practices/references/backend-frontend-devops-exemplars.md +109 -0
  245. package/registry/translation-ledger.json +48 -5
package/dist/cli.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { cac } from "cac";
5
5
  import pc from "picocolors";
6
6
  import { intro, outro, spinner, note, log, select, multiselect, confirm, text } from "@clack/prompts";
7
- import fs15 from "fs-extra";
8
- import path17 from "path";
7
+ import fs20 from "fs-extra";
8
+ import path22 from "path";
9
9
 
10
10
  // src/core/registry.ts
11
11
  import path from "path";
@@ -337,11 +337,42 @@ var RegistryResolver = class _RegistryResolver {
337
337
  }
338
338
  }
339
339
  };
340
+ function loadTranslationLedger(registryDir = "registry") {
341
+ const ledgerPath = path.join(registryDir, "translation-ledger.json");
342
+ const parsed = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
343
+ const entries = Array.isArray(parsed) ? parsed : parsed.entries;
344
+ if (!Array.isArray(entries)) {
345
+ throw new Error(`Declared-Delta Registry error: ${ledgerPath} must be a JSON array of entries or carry an "entries" array.`);
346
+ }
347
+ const validDispositions = /* @__PURE__ */ new Set(["mapped", "approximated", "degraded", "unsupported"]);
348
+ return entries.map((raw, index) => {
349
+ const entry = raw;
350
+ const feature = entry.feature;
351
+ const host = entry.host;
352
+ const disposition = entry.disposition;
353
+ const rationale = entry.rationale;
354
+ if (typeof feature !== "string" || feature.trim() === "") {
355
+ throw new Error(`Declared-Delta Registry error: entries[${index}] needs a non-empty feature.`);
356
+ }
357
+ if (typeof host !== "string" || host.trim() === "") {
358
+ throw new Error(`Declared-Delta Registry error: entries[${index}] ("${feature}") needs a non-empty host.`);
359
+ }
360
+ if (typeof disposition !== "string" || !validDispositions.has(disposition)) {
361
+ throw new Error(
362
+ `Declared-Delta Registry error: entries[${index}] ("${feature}") needs disposition mapped|approximated|degraded|unsupported.`
363
+ );
364
+ }
365
+ if (typeof rationale !== "string" || rationale.trim() === "") {
366
+ throw new Error(`Declared-Delta Registry error: entries[${index}] ("${feature}") needs a rationale.`);
367
+ }
368
+ return { feature, host, disposition, rationale };
369
+ });
370
+ }
340
371
 
341
372
  // src/core/installer.ts
342
- import path6 from "path";
343
- import crypto from "crypto";
344
- import fs4 from "fs-extra";
373
+ import path9 from "path";
374
+ import crypto2 from "crypto";
375
+ import fs7 from "fs-extra";
345
376
 
346
377
  // src/core/adapter.ts
347
378
  import path3 from "path";
@@ -432,6 +463,9 @@ var KNOWN_HOST_IDS = Object.keys(HOST_REGISTRY);
432
463
  function isKnownHost(id) {
433
464
  return Object.prototype.hasOwnProperty.call(HOST_REGISTRY, id);
434
465
  }
466
+ function splitHostList(value) {
467
+ return value.split(/[,\s]+/).filter(Boolean);
468
+ }
435
469
  var SUPPORTED_HOST_IDS = Object.values(HOST_REGISTRY).filter((h) => h.status === "supported").map((h) => h.id);
436
470
  var UNDER_DEVELOPMENT_HOST_IDS = Object.values(HOST_REGISTRY).filter((h) => h.status === "under-development").map((h) => h.id);
437
471
  var PLANNED_HOSTS = [
@@ -453,22 +487,23 @@ function resolveHostProjectDir(host, cwd) {
453
487
  function resolveHostGlobalDir(host, home) {
454
488
  return path2.join(home, ...HOST_REGISTRY[host].globalDirSegments);
455
489
  }
456
- function planInstallTargets(selected) {
490
+ function planInstallTargets(selected, opts = {}) {
457
491
  const normalized = Array.from(
458
492
  new Set(selected.map((s) => typeof s === "string" ? s.trim().toLowerCase() : "").filter(Boolean))
459
493
  );
460
494
  const known = normalized.filter(isKnownHost);
461
495
  const fanout = known.filter((h) => HOST_REGISTRY[h].projectionCapable);
462
496
  const hosts = known.filter((h) => !HOST_REGISTRY[h].projectionCapable);
497
+ const storeShape = hosts.length === 0 && fanout.length === 1 && fanout[0] === "claude" && !opts.canonicalStore && !opts.pluginLane ? "sidecar" : "store";
463
498
  let addedCanonicalStore = false;
464
499
  if (fanout.length > 0 && !hosts.includes("agents")) {
465
500
  hosts.unshift("agents");
466
- addedCanonicalStore = true;
501
+ addedCanonicalStore = storeShape === "store";
467
502
  }
468
503
  if (hosts.length === 0 && fanout.length === 0) {
469
504
  hosts.push("agents");
470
505
  }
471
- return { hosts, fanout, addedCanonicalStore };
506
+ return { hosts, fanout, addedCanonicalStore, storeShape };
472
507
  }
473
508
 
474
509
  // src/core/adapter.ts
@@ -563,6 +598,12 @@ complete the role in the main session.`;
563
598
  } else if (typeof defaultMaxIterations === "number") {
564
599
  cleanFrontmatter.maxIterations = defaultMaxIterations;
565
600
  }
601
+ if (Array.isArray(parsed.skills)) {
602
+ const skills = parsed.skills.filter((s) => typeof s === "string" && s.trim().length > 0);
603
+ if (skills.length > 0) {
604
+ cleanFrontmatter.skills = skills;
605
+ }
606
+ }
566
607
  const frontmatterStr = yaml2.stringify(cleanFrontmatter).trim();
567
608
  const bodyStr = match[2].trim();
568
609
  return `---
@@ -737,7 +778,7 @@ When user tasks require capabilities from: ${addons.join(", ")}, explain the cap
737
778
  const mode = planning?.mode ?? "subagent-first";
738
779
  const budget = planning?.budget;
739
780
  const delegationStep = planning ? mode === "subagent-first" ? "2. **Subagent-First Delegation Policy (ADR 0014)**: execute specialist work through the configured `subagent_*` agent tools (projected under `.cline/agents/`), assigning non-overlapping scopes. Complete specialist work in the main session ONLY if the subagent tools are genuinely absent from this runtime or the task is trivial (single-file read, one-line answer, formatting) \u2014 never as a convenience or speed choice." : "2. **Planner-Orchestrator Delegation Policy (ADR 0015)**: execute specialist work through the configured `subagent_*` agent tools (projected under `.cline/agents/`), assigning non-overlapping scopes. Complete specialist work in the main session ONLY if the subagent tools are genuinely absent from this runtime or the task is trivial (single-file read, one-line answer, formatting) \u2014 never as a convenience or speed choice." : "2. Delegate specialist tasks using the configured `subagent_*` agent tools (projected under `.cline/agents/`) when available, assigning non-overlapping scopes; fall back to Agent Teams (`team_spawn_teammate`) or session subagents as needed.";
740
- const planningSection = planning ? mode === "subagent-first" ? "\n\n## Subagent-First Planning Dialogue Loop (ADR 0014)Run this loop BEFORE any substantive execution on a non-trivial task. Delegation-first is mandatory, not advisory.\n\n### Phase 0 \u2014 User Alignment\nIf the user's brief is ambiguous, grill it Socratically with the user first: `/grill-me` (strategy / non-code) or `/grill-with-docs` (code & docs; writes ADRs and updates CONTEXT.md).\n\n### Phase 0.5 \u2014 Sidekick Clarification\nSpawn at most " + (planning.sidekicks?.max ?? 2) + " relevant specialists (spawnable `subagent_*` tools) INTO this planning conversation to resolve remaining ambiguity. Sidekicks advise you; you relay their questions to the user.\n\n### Phase 1 \u2014 Specialist Council\nHave every relevant specialist return a Scope-of-Work Statement (max " + (budget?.summaryWordCap ?? 150) + " words): (1) my scope, (2) inputs I need from peers, (3) my deliverable per my own workflows, (4) at most 2 open questions.\n\n### Phase 2 \u2014 Delegation Map\nSynthesize the council output into a task \u2192 specialist map and present it to the user BEFORE execution. Then delegate per the map.\n\n### Consultation Budget (hard caps)\n- Planning rounds (orchestrator \u2194 council): max " + (budget?.maxPlanningRounds ?? 2) + "\n- Peer exchanges per specialist pair: max " + (budget?.maxPeerExchangesPerPair ?? 2) + " directed questions\n- Scope-of-Work statement length: max " + (budget?.summaryWordCap ?? 150) + " words\n- Specialist per-invocation iteration cap: maxIterations: " + (budget?.maxIterations ?? 8) + " (rendered into .cline/agents/*.yml)" : "\n\n## Planner-Orchestrator Policy (ADR 0015)Plan solo, delegate execution. This mode replaces the Subagent-First Planning Dialogue Loop for single-discipline domain bundles.\n\n### Phase 0 \u2014 User Alignment (solo)\nIf the user's brief is ambiguous, grill it Socratically yourself: `/grill-me` (strategy / non-code) or `/grill-with-docs` (code & docs). Consult the bundle's skills directly whenever they help you plan \u2014 you have the same skill access as your specialists. Do NOT spawn specialists during planning.\n\n### Planning Aid Boundary\nWhile planning you may consult skills and reason to give the user PROVISIONAL answers and estimates. A concrete deliverable \u2014 data analysis, code, assets, documents \u2014 is specialist work: defer it to the delegation map, never produce it yourself during planning.\n\n### Phase 2 \u2014 Delegation Map (solo-composed)\nCompose the task \u2192 specialist map from your own domain expertise and the skill runbooks, and present it to the user BEFORE execution.\n\n### Execution\nDelegate every deliverable to the configured `subagent_*` agent tools (projected under `.cline/agents/`), assigning non-overlapping scopes. Complete specialist work in the main session ONLY if the subagent tools are genuinely absent from this runtime or the task is trivial (single-file read, one-line answer, formatting) \u2014 never as a convenience or speed choice." : "";
781
+ const planningSection = planning ? mode === "subagent-first" ? "\n\n## Subagent-First Planning Dialogue Loop (ADR 0014)Run this loop BEFORE any substantive execution on a non-trivial task. Delegation-first is mandatory, not advisory.\n\n### Phase 0 \u2014 User Alignment\nIf the user's brief is ambiguous, grill it Socratically with the user first: `/grill-me` (strategy / non-code) or `/grill-with-docs` (code & docs; writes ADRs and updates CONTEXT.md).\n\n### Phase 0.5 \u2014 Sidekick Clarification\nSpawn at most " + (planning.sidekicks?.max ?? 2) + " relevant specialists (spawnable `subagent_*` tools) INTO this planning conversation to resolve remaining ambiguity. Sidekicks advise you; you relay their questions to the user.\n\n### Phase 1 \u2014 Specialist Council\nHave every relevant specialist return a Scope-of-Work Statement (max " + (budget?.summaryWordCap ?? 150) + " words): (1) my scope, (2) inputs I need from peers, (3) my deliverable per my own workflows, (4) at most 2 open questions.\n\n### Phase 2 \u2014 Delegation Map\nSynthesize the council output into a task \u2192 specialist map and present it to the user BEFORE execution. Then delegate per the map.\n\n### Consultation Budget (hard caps)\n- Planning rounds (orchestrator \u2194 council): max " + (budget?.maxPlanningRounds ?? 2) + "\n- Peer exchanges per specialist pair: max " + (budget?.maxPeerExchangesPerPair ?? 2) + " directed questions\n- Scope-of-Work statement length: max " + (budget?.summaryWordCap ?? 150) + " words\n- Specialist per-invocation iteration cap: maxIterations: " + (budget?.maxIterations ?? 8) + " (rendered into .cline/agents/*.yml)" : "\n\n## Planner-Orchestrator Policy (ADR 0015)Plan solo, delegate execution. This mode replaces the Subagent-First Planning Dialogue Loop for single-discipline domain bundles.\n\n### Phase 0 \u2014 User Alignment (solo)\nIf the user's brief is ambiguous, grill it Socratically yourself: `/grill-me` (strategy / non-code) or `/grill-with-docs` (code & docs). Consult the bundle's skills directly whenever they help you plan \u2014 you have the same skill access as your specialists. During planning, specialists are consulted read-only only (the Planning Consultation Phase gate); do not spawn them to produce deliverables.\n\n### Planning Aid Boundary\nWhile planning you may consult skills and reason to give the user PROVISIONAL answers and estimates. A concrete deliverable \u2014 data analysis, code, assets, documents \u2014 is specialist work: defer it to the delegation map, never produce it yourself during planning.\n\n### Phase 2 \u2014 Delegation Map\nCompose the task \u2192 specialist map from your domain expertise, the skill runbooks and the read-only consultation output, and present it to the user BEFORE execution.\n\n### Execution\nDelegate every deliverable to the configured `subagent_*` agent tools (projected under `.cline/agents/`), assigning non-overlapping scopes. Complete specialist work in the main session ONLY if the subagent tools are genuinely absent from this runtime or the task is trivial (single-file read, one-line answer, formatting) \u2014 never as a convenience or speed choice." : "";
741
782
  const personaSection = planning && bundle.personaAliases && Object.keys(bundle.personaAliases).length > 0 ? `
742
783
 
743
784
  ### Persona \u2192 Spawnable Tool Map
@@ -1214,7 +1255,7 @@ var FEATURE_LEDGER = {
1214
1255
  ask_question: { disposition: "approximated", rationale: "Claude exposes AskUserQuestion only in the main conversation (excluded from subagents by default)." },
1215
1256
  generate_image: { disposition: "unsupported", rationale: "Claude Code exposes no image-generation tool; asset creation must happen outside the agent run." },
1216
1257
  skills: { disposition: "degraded", rationale: "Claude subagents discover project skills and can invoke them; the canonical preload list is not injected to avoid paying its token cost on every spawn." },
1217
- mcpServers: { disposition: "degraded", rationale: "Claude supports per-subagent mcpServers, but the canonical descriptor dialect differs; MCP wiring stays host-configured via the MCP location registry (translation deferred)." }
1258
+ mcpServers: { disposition: "mapped", rationale: "Plan 029 A2 \u2014 a canonical `mcpServers:` entry projects into the Claude subagent `mcpServers:` field as a server NAME reference (`- name: <server>`), and the matching grant lands in `tools:` (`mcp__<server>`, or pinned read-only tool names for a read-only role). Credentials, `env` blocks and inline server definitions are never projected (Risk R4)." }
1218
1259
  };
1219
1260
  var MARKER_PROFILE = "claude";
1220
1261
  var MARKER_PREFIX = "managed-by: agents-united";
@@ -1222,11 +1263,28 @@ var HOST = "claude";
1222
1263
  var ENTRYPOINT_RULES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md", "GEMINI.md", "AGENTS.md", "CURSOR.md"]);
1223
1264
  var COORDINATOR_BASELINE_TOOLS = ["Read", "Write", "Edit", "Bash", "Grep", "Glob"];
1224
1265
  var READ_ONLY_EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "NotebookEdit", "Bash"]);
1266
+ var MCP_SERVERS_KEY = "mcpServers";
1267
+ var READ_ONLY_MCP_TOOL_GRANTS = {
1268
+ // Only GitHub tools whose names are read-only verbs (`search_*`, `get_*`, `list_*`, `*_read`).
1269
+ github: [
1270
+ "mcp__github__search_code",
1271
+ "mcp__github__get_file_contents",
1272
+ "mcp__github__list_pull_requests",
1273
+ "mcp__github__pull_request_read"
1274
+ ],
1275
+ // Context7 is documentation lookup only, so its published tools are read-only by construction.
1276
+ // Plan 029 Step 3 pin correction: `get-library-docs` is Context7's DEPRECATED tool name; the
1277
+ // current surface is `resolve-library-id` + `query-docs` (verified against the live MCP server).
1278
+ context7: ["mcp__context7__resolve-library-id", "mcp__context7__query-docs"]
1279
+ };
1225
1280
  var FRONTMATTER_REGEX2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
1226
1281
  var RUNTIME_NOTE = `## Claude runtime note
1227
1282
 
1228
1283
  Delegation runs through the Agent tool: the coordinator spawns the specialists named in its own tools
1229
- allowlist; specialists hold no Agent tool and never spawn peers (the coordinator relays and wakes them). Canonical tool names in this prompt were rewritten to their
1284
+ allowlist; specialists hold no Agent tool and never spawn peers (the coordinator relays and wakes them).
1285
+ Outside Agent Teams (\`--teams\`), specialists cannot message each other by name: only the coordinator holds
1286
+ their agent IDs, so relay mode is the default and team mode exists only when the brief says so.
1287
+ Canonical tool names in this prompt were rewritten to their
1230
1288
  Claude equivalents; a fenced code block may still show the original spelling because code is preserved
1231
1289
  byte-for-byte. A subagent does not hand results to a peer: its final report is returned to the
1232
1290
  conversation that spawned it, and on Claude Code v2.1.271+ in auto mode the runtime delivers it through the
@@ -1367,6 +1425,47 @@ var ClaudeProjector = class _ClaudeProjector {
1367
1425
  const meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1368
1426
  return { meta, body: match[2].replace(/^\r?\n/, "") };
1369
1427
  }
1428
+ /**
1429
+ * Plan 029 A2 / Risk R4 — the servers a canonical role declares, by NAME only.
1430
+ *
1431
+ * The canonical dialect is a sequence of `- name: <server>` mapping objects (a projected-style
1432
+ * plain string is accepted too). Anything else is a fail-fast error: an inline server definition
1433
+ * (`command`, `args`, `env`, `headers`, `url`, …) would put a credential-shaped block into a
1434
+ * projected artifact — exactly what Risk R4 forbids — so it never reaches the output and is
1435
+ * never silently dropped either (ADR 0018 decision 9).
1436
+ */
1437
+ static declaredMcpServers(meta, canonicalRelPath) {
1438
+ const raw = meta[MCP_SERVERS_KEY];
1439
+ if (raw === void 0 || raw === null) return [];
1440
+ if (!Array.isArray(raw)) {
1441
+ throw new Error(
1442
+ `Cannot project ${canonicalRelPath}: ${MCP_SERVERS_KEY} must be a sequence of "- name: <server>" entries (server names only; an inline server definition is never projected).`
1443
+ );
1444
+ }
1445
+ const names = [];
1446
+ raw.forEach((entry, index) => {
1447
+ if (typeof entry === "string" && entry.trim().length > 0) {
1448
+ if (!names.includes(entry.trim())) names.push(entry.trim());
1449
+ return;
1450
+ }
1451
+ if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
1452
+ const record = entry;
1453
+ const keys = Object.keys(record);
1454
+ const name = record.name;
1455
+ if (typeof name === "string" && name.trim().length > 0 && keys.every((key) => key === "name")) {
1456
+ if (!names.includes(name.trim())) names.push(name.trim());
1457
+ return;
1458
+ }
1459
+ throw new Error(
1460
+ `Cannot project ${canonicalRelPath}: ${MCP_SERVERS_KEY} entry #${index + 1} carries [${keys.join(", ")}] \u2014 servers are referenced by NAME only, never by an inline server definition (Risk R4: no credential may reach a projected artifact).`
1461
+ );
1462
+ }
1463
+ throw new Error(
1464
+ `Cannot project ${canonicalRelPath}: ${MCP_SERVERS_KEY} entry #${index + 1} is not a server name reference (expected "- name: <server>").`
1465
+ );
1466
+ });
1467
+ return names;
1468
+ }
1370
1469
  static ledgerEntry(feature) {
1371
1470
  const known = FEATURE_LEDGER[feature];
1372
1471
  if (!known) return void 0;
@@ -1520,7 +1619,17 @@ var ClaudeProjector = class _ClaudeProjector {
1520
1619
  tools.length = 0;
1521
1620
  tools.push(...restricted);
1522
1621
  }
1622
+ const declaredServers = _ClaudeProjector.declaredMcpServers(meta, canonicalRelPath);
1623
+ for (const server of declaredServers) {
1624
+ const grants = permission === "plan" ? READ_ONLY_MCP_TOOL_GRANTS[server] ?? [] : [`mcp__${server}`];
1625
+ for (const grant of grants) {
1626
+ if (!tools.includes(grant)) tools.push(grant);
1627
+ }
1628
+ }
1523
1629
  out.tools = tools;
1630
+ if (declaredServers.length > 0) {
1631
+ out.mcpServers = declaredServers.map((server) => ({ name: server }));
1632
+ }
1524
1633
  if (permission) out.permissionMode = permission;
1525
1634
  const declaredModel = typeof meta.model === "string" ? meta.model : void 0;
1526
1635
  const mappedModel = declaredModel && declaredModel !== "inherit" ? _ClaudeProjector.CLAUDE_DIALECT.modelMap[declaredModel] ?? declaredModel : void 0;
@@ -1733,14 +1842,13 @@ ${rewritten.body}
1733
1842
  content: _ClaudeProjector.renderSkill(await fs3.readFile(skillFile, "utf8"), canonicalRel).content,
1734
1843
  managedMarker: true
1735
1844
  });
1736
- const entries = (await fs3.readdir(skillDir, { withFileTypes: true })).filter((entry) => entry.name !== "SKILL.md").sort((a, b) => a.name.localeCompare(b.name));
1737
- for (const entry of entries) {
1738
- if (!entry.isFile()) continue;
1845
+ const resourceRels = (await _ClaudeProjector.listSkillResources(skillDir)).filter((rel) => rel !== "SKILL.md");
1846
+ for (const rel of resourceRels) {
1739
1847
  artifacts.push({
1740
1848
  kind: "skill",
1741
- canonical: `skills/${skillName}/${entry.name}`,
1742
- relPath: `.claude/skills/${normalized}/${entry.name}`,
1743
- sourceFilePath: path5.join(skillDir, entry.name),
1849
+ canonical: `skills/${skillName}/${rel}`,
1850
+ relPath: `.claude/skills/${normalized}/${rel}`,
1851
+ sourceFilePath: path5.join(skillDir, ...rel.split("/")),
1744
1852
  managedMarker: false
1745
1853
  });
1746
1854
  }
@@ -1762,6 +1870,19 @@ ${rewritten.body}
1762
1870
  }
1763
1871
  return artifacts;
1764
1872
  }
1873
+ /**
1874
+ * Every file under a skill folder, as sorted POSIX-style paths relative to it
1875
+ * (deterministic across platforms, so projection plans and lockfile keys never differ).
1876
+ */
1877
+ static async listSkillResources(skillDir, prefix = "") {
1878
+ const out = [];
1879
+ for (const entry of await fs3.readdir(path5.join(skillDir, ...prefix.split("/").filter(Boolean)), { withFileTypes: true })) {
1880
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
1881
+ if (entry.isDirectory()) out.push(...await _ClaudeProjector.listSkillResources(skillDir, rel));
1882
+ else if (entry.isFile()) out.push(rel);
1883
+ }
1884
+ return out.sort((a, b) => a.localeCompare(b));
1885
+ }
1765
1886
  /** Claude plugin package name rule; every bundle in `registry/bundles.json` satisfies it today. */
1766
1887
  static PLUGIN_NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
1767
1888
  /**
@@ -1844,6 +1965,245 @@ ${rewritten.body}
1844
1965
  }
1845
1966
  };
1846
1967
 
1968
+ // src/core/state-dir.ts
1969
+ import os2 from "os";
1970
+ import path6 from "path";
1971
+ import fs4 from "fs-extra";
1972
+ var SIDECAR_DIR = ".claude/.agents-united";
1973
+ var STORE_DIR = ".agents";
1974
+ var LOCKFILE = "agents-united.json";
1975
+ function isSidecarDir(stateDir) {
1976
+ const resolved = path6.resolve(stateDir);
1977
+ return path6.basename(resolved) === ".agents-united" && path6.basename(path6.dirname(resolved)) === ".claude";
1978
+ }
1979
+ function workspaceRootOf(stateDir) {
1980
+ const resolved = path6.resolve(stateDir);
1981
+ return isSidecarDir(resolved) ? path6.dirname(path6.dirname(resolved)) : path6.dirname(resolved);
1982
+ }
1983
+ function stateDirFor(root, shape) {
1984
+ return shape === "sidecar" ? path6.join(root, ...SIDECAR_DIR.split("/")) : path6.join(root, STORE_DIR);
1985
+ }
1986
+ function resolveStateDir(scope = "project", overrideDir, opts = {}) {
1987
+ if (overrideDir) return path6.resolve(overrideDir);
1988
+ const root = scope === "global" ? opts.home ?? os2.homedir() : opts.cwd ?? process.cwd();
1989
+ const store = stateDirFor(root, "store");
1990
+ if (fs4.existsSync(path6.join(store, LOCKFILE))) return store;
1991
+ const sidecar = stateDirFor(root, "sidecar");
1992
+ if (fs4.existsSync(path6.join(sidecar, LOCKFILE))) return sidecar;
1993
+ return opts.preferShape === "sidecar" ? sidecar : store;
1994
+ }
1995
+
1996
+ // src/core/session-guard.ts
1997
+ import os3 from "os";
1998
+ import path7 from "path";
1999
+ import crypto from "crypto";
2000
+ import fs5 from "fs-extra";
2001
+ var GUARD_MARKER = "Blocked by agents-united guard";
2002
+ var SESSION_GUARD_FILES = {
2003
+ project: ".claude/settings.json",
2004
+ local: ".claude/settings.local.json",
2005
+ user: ".claude/settings.json"
2006
+ };
2007
+ function resolveSessionGuardFile(variant, workspaceRoot) {
2008
+ const base = variant === "user" ? os3.homedir() : workspaceRoot;
2009
+ return path7.join(base, ...SESSION_GUARD_FILES[variant].split("/"));
2010
+ }
2011
+ function variantOfRecordedFile(file) {
2012
+ if (path7.isAbsolute(file)) return "user";
2013
+ return file.endsWith("settings.local.json") ? "local" : "project";
2014
+ }
2015
+ function guardHandlerHash() {
2016
+ return crypto.createHash("sha256").update(JSON.stringify(guardHandler())).digest("hex").slice(0, 16);
2017
+ }
2018
+ function sessionGuardSnippet() {
2019
+ return JSON.stringify({ hooks: managedGuardHooks() }, null, 2);
2020
+ }
2021
+ var isOursAnyVersion = (handler) => handler.command === "node" && Array.isArray(handler.args) && handler.args[0] === "-e" && typeof handler.args[1] === "string" && handler.args[1].includes(GUARD_MARKER);
2022
+ var isCurrentGuard = (handler) => JSON.stringify({ type: handler.type, command: handler.command, args: handler.args }) === JSON.stringify(guardHandler());
2023
+ var groupIsOurs = (group) => Array.isArray(group.hooks) && group.hooks.some(isOursAnyVersion);
2024
+ async function parse(file) {
2025
+ if (!await fs5.pathExists(file)) return "absent";
2026
+ const text2 = await fs5.readFile(file, "utf8");
2027
+ let value;
2028
+ try {
2029
+ value = JSON.parse(text2.replace(/^/, ""));
2030
+ } catch {
2031
+ return "invalid";
2032
+ }
2033
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "invalid";
2034
+ const hooks = value.hooks;
2035
+ if (hooks !== void 0 && (hooks === null || typeof hooks !== "object" || Array.isArray(hooks))) return "invalid";
2036
+ if (hooks?.PreToolUse !== void 0 && !Array.isArray(hooks.PreToolUse)) return "invalid";
2037
+ const indentMatch = text2.match(/\n([ \t]+)"/);
2038
+ return {
2039
+ settings: value,
2040
+ indent: indentMatch ? indentMatch[1] : 2,
2041
+ eol: text2.includes("\r\n") ? "\r\n" : "\n",
2042
+ trailingNewline: /\r?\n$/.test(text2)
2043
+ };
2044
+ }
2045
+ async function write(file, parsed, settings) {
2046
+ let text2 = JSON.stringify(settings, null, parsed.indent);
2047
+ if (parsed.eol === "\r\n") text2 = text2.replace(/\n/g, "\r\n");
2048
+ if (parsed.trailingNewline) text2 += parsed.eol;
2049
+ await fs5.ensureDir(path7.dirname(file));
2050
+ await fs5.writeFile(file, text2, "utf8");
2051
+ }
2052
+ async function mergeSessionGuard(file, opts = {}) {
2053
+ const handlerHash = guardHandlerHash();
2054
+ const parsed = await parse(file);
2055
+ if (parsed === "invalid") return { status: "skipped-invalid", handlerHash };
2056
+ const created = parsed === "absent";
2057
+ const base = created ? { indent: 2, eol: "\n", trailingNewline: true } : parsed;
2058
+ const settings = created ? {} : parsed.settings;
2059
+ const hooks = settings.hooks ??= {};
2060
+ let pre = hooks.PreToolUse ??= [];
2061
+ if (opts.replaceHash && opts.replaceHash !== handlerHash) {
2062
+ pre = hooks.PreToolUse = pre.filter((group) => !(groupIsOurs(group) && !group.hooks.some(isCurrentGuard)));
2063
+ }
2064
+ let changed = false;
2065
+ let modified = false;
2066
+ for (const wanted of managedGuardHooks().PreToolUse) {
2067
+ const sameMatcher = pre.filter((group) => group.matcher === wanted.matcher && groupIsOurs(group));
2068
+ if (sameMatcher.some((group) => group.hooks.some(isCurrentGuard))) continue;
2069
+ if (sameMatcher.length > 0) {
2070
+ modified = true;
2071
+ continue;
2072
+ }
2073
+ pre.push(JSON.parse(JSON.stringify(wanted)));
2074
+ changed = true;
2075
+ }
2076
+ if (!changed) return { status: modified ? "modified" : "unchanged", handlerHash };
2077
+ await write(file, base, settings);
2078
+ return { status: created ? "created" : "merged", handlerHash };
2079
+ }
2080
+ async function removeSessionGuard(file, opts) {
2081
+ const parsed = await parse(file);
2082
+ if (parsed === "absent") return "absent";
2083
+ if (parsed === "invalid") return "skipped-invalid";
2084
+ const { settings } = parsed;
2085
+ const pre = settings.hooks?.PreToolUse;
2086
+ if (!pre || !pre.some(groupIsOurs)) return "absent";
2087
+ const kept = pre.filter((group) => !groupIsOurs(group));
2088
+ if (kept.length > 0) {
2089
+ settings.hooks.PreToolUse = kept;
2090
+ } else {
2091
+ delete settings.hooks.PreToolUse;
2092
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
2093
+ }
2094
+ if (opts.createdFile && Object.keys(settings).length === 0) {
2095
+ await fs5.remove(file);
2096
+ return "deleted-file";
2097
+ }
2098
+ await write(file, parsed, settings);
2099
+ return "removed";
2100
+ }
2101
+ async function inspectSessionGuard(file) {
2102
+ const parsed = await parse(file);
2103
+ if (parsed === "absent" || parsed === "invalid") return parsed === "absent" ? "absent" : "skipped-invalid";
2104
+ const pre = parsed.settings.hooks?.PreToolUse ?? [];
2105
+ let allCurrent = true;
2106
+ let anyOurs = false;
2107
+ for (const wanted of managedGuardHooks().PreToolUse) {
2108
+ const ours = pre.filter((group) => group.matcher === wanted.matcher && groupIsOurs(group));
2109
+ if (ours.length > 0) anyOurs = true;
2110
+ if (!ours.some((group) => group.hooks.some(isCurrentGuard))) allCurrent = false;
2111
+ }
2112
+ if (allCurrent) return "wired";
2113
+ return anyOurs ? "modified" : "missing";
2114
+ }
2115
+
2116
+ // src/core/permission-preset.ts
2117
+ import path8 from "path";
2118
+ import fs6 from "fs-extra";
2119
+ var VERIFY_COMMANDS = [
2120
+ "git status",
2121
+ "git diff",
2122
+ "git log",
2123
+ "npx tsc --noEmit",
2124
+ "npx vitest run",
2125
+ "npx eslint"
2126
+ ];
2127
+ var BUILD_COMMANDS = [...VERIFY_COMMANDS, "npm install", "npm run", "npm test"];
2128
+ var PERMISSION_TIER_COMMANDS = {
2129
+ verify: VERIFY_COMMANDS,
2130
+ build: BUILD_COMMANDS
2131
+ };
2132
+ function claudeAllowEntries(tier) {
2133
+ return PERMISSION_TIER_COMMANDS[tier].map((cmd) => `Bash(${cmd} *)`);
2134
+ }
2135
+ var SUPPORTED_PERMISSION_PRESET_HOSTS = ["claude"];
2136
+ async function parse2(file) {
2137
+ if (!await fs6.pathExists(file)) return "absent";
2138
+ const text2 = await fs6.readFile(file, "utf8");
2139
+ let value;
2140
+ try {
2141
+ value = JSON.parse(text2.replace(/^/, ""));
2142
+ } catch {
2143
+ return "invalid";
2144
+ }
2145
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return "invalid";
2146
+ const permissions = value.permissions;
2147
+ if (permissions !== void 0 && (permissions === null || typeof permissions !== "object" || Array.isArray(permissions))) return "invalid";
2148
+ if (permissions?.allow !== void 0 && !Array.isArray(permissions.allow)) return "invalid";
2149
+ const indentMatch = text2.match(/\n([ \t]+)"/);
2150
+ return {
2151
+ settings: value,
2152
+ indent: indentMatch ? indentMatch[1] : 2,
2153
+ eol: text2.includes("\r\n") ? "\r\n" : "\n",
2154
+ trailingNewline: /\r?\n$/.test(text2)
2155
+ };
2156
+ }
2157
+ async function write2(file, parsed, settings) {
2158
+ let text2 = JSON.stringify(settings, null, parsed.indent);
2159
+ if (parsed.eol === "\r\n") text2 = text2.replace(/\n/g, "\r\n");
2160
+ if (parsed.trailingNewline) text2 += parsed.eol;
2161
+ await fs6.ensureDir(path8.dirname(file));
2162
+ await fs6.writeFile(file, text2, "utf8");
2163
+ }
2164
+ async function mergePermissionPreset(file, tier) {
2165
+ const wanted = claudeAllowEntries(tier);
2166
+ const parsed = await parse2(file);
2167
+ if (parsed === "invalid") return { status: "skipped-invalid", entries: [] };
2168
+ const created = parsed === "absent";
2169
+ const base = created ? { indent: 2, eol: "\n", trailingNewline: true } : parsed;
2170
+ const settings = created ? {} : parsed.settings;
2171
+ const permissions = settings.permissions ??= {};
2172
+ const allow = permissions.allow ??= [];
2173
+ const added = [];
2174
+ for (const entry of wanted) {
2175
+ if (!allow.includes(entry)) {
2176
+ allow.push(entry);
2177
+ added.push(entry);
2178
+ }
2179
+ }
2180
+ if (added.length === 0) return { status: "unchanged", entries: wanted };
2181
+ await write2(file, base, settings);
2182
+ return { status: created ? "created" : "merged", entries: wanted };
2183
+ }
2184
+ async function removePermissionPreset(file, entries, opts) {
2185
+ const parsed = await parse2(file);
2186
+ if (parsed === "absent") return "absent";
2187
+ if (parsed === "invalid") return "skipped-invalid";
2188
+ const { settings } = parsed;
2189
+ const allow = settings.permissions?.allow;
2190
+ if (!Array.isArray(allow)) return "absent";
2191
+ const owned = new Set(entries);
2192
+ const kept = allow.filter((e) => !(typeof e === "string" && owned.has(e)));
2193
+ if (kept.length > 0) {
2194
+ settings.permissions.allow = kept;
2195
+ } else {
2196
+ delete settings.permissions.allow;
2197
+ if (Object.keys(settings.permissions).length === 0) delete settings.permissions;
2198
+ }
2199
+ if (opts.createdFile && Object.keys(settings).length === 0) {
2200
+ await fs6.remove(file);
2201
+ return "deleted-file";
2202
+ }
2203
+ await write2(file, parsed, settings);
2204
+ return "removed";
2205
+ }
2206
+
1847
2207
  // src/core/installer.ts
1848
2208
  import YAML2 from "yaml";
1849
2209
 
@@ -1867,13 +2227,13 @@ var InstallEngine = class _InstallEngine {
1867
2227
  this.registry = registry2 || new RegistryResolver();
1868
2228
  }
1869
2229
  async calculateHash(filePath) {
1870
- const buffer = await fs4.readFile(filePath);
1871
- return "sha256:" + crypto.createHash("sha256").update(buffer).digest("hex");
2230
+ const buffer = await fs7.readFile(filePath);
2231
+ return "sha256:" + crypto2.createHash("sha256").update(buffer).digest("hex");
1872
2232
  }
1873
2233
  async readLockfile(lockfilePath) {
1874
- if (await fs4.pathExists(lockfilePath)) {
2234
+ if (await fs7.pathExists(lockfilePath)) {
1875
2235
  try {
1876
- return await fs4.readJson(lockfilePath);
2236
+ return await fs7.readJson(lockfilePath);
1877
2237
  } catch {
1878
2238
  }
1879
2239
  }
@@ -1916,24 +2276,24 @@ var InstallEngine = class _InstallEngine {
1916
2276
  return options.scope || "project";
1917
2277
  }
1918
2278
  async deployFile(src, dest, method, force) {
1919
- await fs4.ensureDir(path6.dirname(dest));
1920
- if (await fs4.pathExists(dest) || await fs4.pathExists(dest).catch(() => false)) {
2279
+ await fs7.ensureDir(path9.dirname(dest));
2280
+ if (await fs7.pathExists(dest) || await fs7.pathExists(dest).catch(() => false)) {
1921
2281
  if (force) {
1922
- await fs4.remove(dest);
2282
+ await fs7.remove(dest);
1923
2283
  }
1924
2284
  }
1925
2285
  if (method === "symlink") {
1926
2286
  try {
1927
- const stat = await fs4.stat(src);
2287
+ const stat = await fs7.stat(src);
1928
2288
  const type = stat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
1929
- await fs4.ensureSymlink(src, dest, type);
2289
+ await fs7.ensureSymlink(src, dest, type);
1930
2290
  return "symlink";
1931
2291
  } catch {
1932
- await fs4.copy(src, dest, { overwrite: true });
2292
+ await fs7.copy(src, dest, { overwrite: true });
1933
2293
  return "copy";
1934
2294
  }
1935
2295
  }
1936
- await fs4.copy(src, dest, { overwrite: true });
2296
+ await fs7.copy(src, dest, { overwrite: true });
1937
2297
  return "copy";
1938
2298
  }
1939
2299
  /**
@@ -1941,8 +2301,8 @@ var InstallEngine = class _InstallEngine {
1941
2301
  * copies of canonical assets, so their bytes differ from the source.
1942
2302
  */
1943
2303
  async deployProjection(dest, content) {
1944
- await fs4.ensureDir(path6.dirname(dest));
1945
- await fs4.writeFile(dest, content, "utf8");
2304
+ await fs7.ensureDir(path9.dirname(dest));
2305
+ await fs7.writeFile(dest, content, "utf8");
1946
2306
  }
1947
2307
  /**
1948
2308
  * Computes the canonical `.agents/` target dir and the workspace root that contains
@@ -1950,27 +2310,27 @@ var InstallEngine = class _InstallEngine {
1950
2310
  */
1951
2311
  static projectionRoot(scope, targetDir) {
1952
2312
  const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", targetDir);
1953
- return { agentsTarget, root: path6.resolve(path6.dirname(agentsTarget)) };
2313
+ return { agentsTarget, root: workspaceRootOf(agentsTarget) };
1954
2314
  }
1955
2315
  toPosix(p) {
1956
- return p.split(path6.sep).join("/");
2316
+ return p.split(path9.sep).join("/");
1957
2317
  }
1958
2318
  canonicalRelAgent(agentFile) {
1959
2319
  return `.agents/agents/${agentFile}`;
1960
2320
  }
1961
2321
  lockRel(subdir, file) {
1962
- return path6.join(subdir, file);
2322
+ return path9.join(subdir, file);
1963
2323
  }
1964
2324
  destForProjection(host, root, file) {
1965
2325
  const def = HOST_REGISTRY[host];
1966
2326
  const base = resolveHostProjectDir(host, root);
1967
- if (def.agentsSubdir) return path6.join(base, def.agentsSubdir, file);
2327
+ if (def.agentsSubdir) return path9.join(base, def.agentsSubdir, file);
1968
2328
  return base;
1969
2329
  }
1970
2330
  recordProjectedTo(lockfile, relKey, projPath) {
1971
2331
  const normProj = this.toPosix(projPath);
1972
2332
  const posixKey = relKey.replace(/\\/g, "/");
1973
- const sysKey = relKey.replace(/\//g, path6.sep);
2333
+ const sysKey = relKey.replace(/\//g, path9.sep);
1974
2334
  const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : relKey;
1975
2335
  if (!lockfile.files[targetKey]) {
1976
2336
  lockfile.files[targetKey] = { hash: "", installedAt: (/* @__PURE__ */ new Date()).toISOString() };
@@ -1984,7 +2344,7 @@ var InstallEngine = class _InstallEngine {
1984
2344
  removeProjectedTo(lockfile, canonicalRelPath, projectedRelPath) {
1985
2345
  const normProj = this.toPosix(projectedRelPath);
1986
2346
  const posixKey = canonicalRelPath.replace(/\\/g, "/");
1987
- const sysKey = canonicalRelPath.replace(/\//g, path6.sep);
2347
+ const sysKey = canonicalRelPath.replace(/\//g, path9.sep);
1988
2348
  const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : canonicalRelPath;
1989
2349
  const asset = lockfile.files[targetKey];
1990
2350
  if (asset && asset.projectedTo) {
@@ -2041,7 +2401,7 @@ var InstallEngine = class _InstallEngine {
2041
2401
  if (prefixes.length === 0 || producedByCanonical.size === 0) return;
2042
2402
  for (const [canonical, producedPaths] of producedByCanonical) {
2043
2403
  const posixKey = canonical.replace(/\\/g, "/");
2044
- const sysKey = canonical.replace(/\//g, path6.sep);
2404
+ const sysKey = canonical.replace(/\//g, path9.sep);
2045
2405
  const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : null;
2046
2406
  if (!targetKey) continue;
2047
2407
  const asset = lockfile.files[targetKey];
@@ -2080,9 +2440,9 @@ var InstallEngine = class _InstallEngine {
2080
2440
  if (proj.host === host && !newPaths.has(projRelPath) && proj.owners.includes(bundleName)) {
2081
2441
  proj.owners = proj.owners.filter((o) => o !== bundleName);
2082
2442
  if (proj.owners.length === 0) {
2083
- const absProjection = path6.join(root, projRelPath);
2084
- if (await fs4.pathExists(absProjection)) {
2085
- await fs4.remove(absProjection);
2443
+ const absProjection = path9.join(root, projRelPath);
2444
+ if (await fs7.pathExists(absProjection)) {
2445
+ await fs7.remove(absProjection);
2086
2446
  await this.removeEmptyProjectionDirs(root, projRelPath);
2087
2447
  }
2088
2448
  delete lockfile.projections[projRelPath];
@@ -2094,9 +2454,9 @@ var InstallEngine = class _InstallEngine {
2094
2454
  }
2095
2455
  }
2096
2456
  for (const artifact of artifacts) {
2097
- const dest = path6.join(root, artifact.relPath);
2098
- if (await fs4.pathExists(dest) && !force) {
2099
- const existing = await fs4.readFile(dest, "utf8").catch(() => null);
2457
+ const dest = path9.join(root, artifact.relPath);
2458
+ if (await fs7.pathExists(dest) && !force) {
2459
+ const existing = await fs7.readFile(dest, "utf8").catch(() => null);
2100
2460
  if (existing !== null) {
2101
2461
  if (artifact.managedMarker && !HostProjector.hasManagedMarker(existing)) {
2102
2462
  throw new Error(
@@ -2115,8 +2475,8 @@ var InstallEngine = class _InstallEngine {
2115
2475
  if (artifact.content !== void 0) {
2116
2476
  await this.deployProjection(dest, artifact.content);
2117
2477
  } else if (artifact.sourceFilePath) {
2118
- await fs4.ensureDir(path6.dirname(dest));
2119
- await fs4.copy(artifact.sourceFilePath, dest, { overwrite: true });
2478
+ await fs7.ensureDir(path9.dirname(dest));
2479
+ await fs7.copy(artifact.sourceFilePath, dest, { overwrite: true });
2120
2480
  }
2121
2481
  const deployedHash = await this.calculateHash(dest);
2122
2482
  lockfile.projections = lockfile.projections || {};
@@ -2167,6 +2527,106 @@ var InstallEngine = class _InstallEngine {
2167
2527
  if (typeof options.pluginLane === "boolean") return options.pluginLane;
2168
2528
  return lockfile?.pluginLane === true;
2169
2529
  }
2530
+ /**
2531
+ * Plan 023 B (ADR 0022, D4) — resolve the state dir for this install. An explicit `targetDir`
2532
+ * is a location hint; a requested (or implied) store shape always lands in `.agents/`, and a
2533
+ * sidecar is used only when nothing needs the store: any non-Claude fan-out or the plugin lane
2534
+ * (both write under `.agents/plugins/`) forces the store (strangler-safe, ADR 0022 decision 4).
2535
+ */
2536
+ static resolveInstallStateDir(scope, options, fanoutHosts) {
2537
+ const discovered = resolveStateDir(scope, options.targetDir, { preferShape: options.storeShape ?? "store" });
2538
+ const needsStore = options.storeShape === "store" || fanoutHosts.some((h) => h !== "claude") || options.pluginLane === true;
2539
+ if (needsStore && isSidecarDir(discovered)) return stateDirFor(workspaceRootOf(discovered), "store");
2540
+ return discovered;
2541
+ }
2542
+ /** Move an existing sidecar into the store dir (ADR 0022 upgrade path); a no-op otherwise. */
2543
+ static async upgradeSidecarToStore(stateDir) {
2544
+ if (isSidecarDir(stateDir)) return;
2545
+ const sidecar = stateDirFor(workspaceRootOf(stateDir), "sidecar");
2546
+ if (!await fs7.pathExists(path9.join(sidecar, "agents-united.json"))) return;
2547
+ if (await fs7.pathExists(path9.join(stateDir, "agents-united.json"))) return;
2548
+ await fs7.ensureDir(stateDir);
2549
+ for (const entry of await fs7.readdir(sidecar)) {
2550
+ await fs7.move(path9.join(sidecar, entry), path9.join(stateDir, entry), { overwrite: true });
2551
+ }
2552
+ await fs7.remove(sidecar);
2553
+ }
2554
+ /**
2555
+ * Plan 023 A (owner D1–D2) — the plain-session guard. Runs only when the Claude lane runs.
2556
+ * An explicit `options.sessionGuard` wins and is recorded; otherwise the recorded decision is
2557
+ * re-applied (sticky yes re-merges idempotently, sticky no writes nothing); never decided ⇒
2558
+ * nothing is written (the CLI asks). `project`/`local` are workspace files and are ignored for
2559
+ * global installs; `user` (home settings) is honoured only when requested explicitly.
2560
+ */
2561
+ async applySessionGuard(scope, workspaceRoot, lockfile, options, projections) {
2562
+ const recorded = lockfile.sessionGuard;
2563
+ let variant = options.sessionGuard;
2564
+ if (variant === void 0 && recorded) {
2565
+ variant = "off" in recorded ? false : variantOfRecordedFile(recorded.file);
2566
+ }
2567
+ if (variant === void 0) return;
2568
+ if (variant === false) {
2569
+ lockfile.sessionGuard = { off: true };
2570
+ return;
2571
+ }
2572
+ if (scope === "global" && variant !== "user") return;
2573
+ const file = resolveSessionGuardFile(variant, workspaceRoot);
2574
+ const recordedFile = variant === "user" ? file : this.toPosix(path9.relative(workspaceRoot, file));
2575
+ const sameFileRecord = recorded && !("off" in recorded) && recorded.file === recordedFile ? recorded : void 0;
2576
+ const existedBefore = await fs7.pathExists(file);
2577
+ const result = await mergeSessionGuard(file, { replaceHash: sameFileRecord?.handlerHash });
2578
+ lockfile.sessionGuard = {
2579
+ file: recordedFile,
2580
+ handlerHash: result.handlerHash,
2581
+ createdFile: sameFileRecord ? sameFileRecord.createdFile : !existedBefore
2582
+ };
2583
+ const warnings = [];
2584
+ if (result.status === "skipped-invalid") {
2585
+ warnings.push(`Session guard not written: ${recordedFile} is not valid JSON (comments or trailing commas?). It was left untouched \u2014 paste this into it by hand:
2586
+ ${sessionGuardSnippet()}`);
2587
+ } else if (result.status === "modified") {
2588
+ warnings.push(`Session guard in ${recordedFile} was edited by hand; left as-is (run with --force after removing it to restore).`);
2589
+ }
2590
+ projections.push({ host: "claude", path: recordedFile, warnings });
2591
+ }
2592
+ /**
2593
+ * Plan 024 S4 (owner decisions E2/E4) — the opt-in command-permission preset. Written ONLY to
2594
+ * `.claude/settings.local.json`, never the shared `.claude/settings.json`, regardless of what
2595
+ * `sessionGuard` variant is also requested. Off by default, never implied by `-y`. Applies to
2596
+ * every fan-out host that has a verified renderer (Claude only today); any other requested host
2597
+ * gets an honest "not supported yet" warning instead of a guessed file.
2598
+ */
2599
+ async applyPermissionPreset(scope, workspaceRoot, lockfile, options, fanoutHosts, projections) {
2600
+ const recorded = lockfile.permissionPreset;
2601
+ const tier = options.permissionPreset ?? (recorded && !("off" in recorded) ? recorded.tier : void 0);
2602
+ if (tier === void 0) return;
2603
+ if (tier === false) {
2604
+ lockfile.permissionPreset = { off: true };
2605
+ return;
2606
+ }
2607
+ if (scope === "global") return;
2608
+ const unsupported = fanoutHosts.filter((h) => h !== "claude" && !SUPPORTED_PERMISSION_PRESET_HOSTS.includes(h));
2609
+ for (const host of unsupported) {
2610
+ projections.push({ host, path: "", warnings: [`Permission preset not supported for ${host} yet \u2014 no file written for it. Claude still received it if selected.`] });
2611
+ }
2612
+ if (!fanoutHosts.includes("claude")) return;
2613
+ const file = path9.join(workspaceRoot, ".claude", "settings.local.json");
2614
+ const recordedFile = this.toPosix(path9.relative(workspaceRoot, file));
2615
+ const existedBefore = await fs7.pathExists(file);
2616
+ const result = await mergePermissionPreset(file, tier);
2617
+ const warnings = [];
2618
+ if (result.status === "skipped-invalid") {
2619
+ warnings.push(`Permission preset not written: ${recordedFile} is not valid JSON (comments or trailing commas?). Left untouched.`);
2620
+ } else {
2621
+ lockfile.permissionPreset = {
2622
+ tier,
2623
+ file: recordedFile,
2624
+ entries: result.entries,
2625
+ createdFile: recorded && !("off" in recorded) ? recorded.createdFile : !existedBefore
2626
+ };
2627
+ }
2628
+ projections.push({ host: "claude", path: recordedFile, warnings });
2629
+ }
2170
2630
  /**
2171
2631
  * ADR 0018 — the host-specific projection plan, the single input that differs per host once
2172
2632
  * `applyCompoundLane` shared the mechanics. `pluginLane` (Plan 016 decision 13) is honoured for
@@ -2184,16 +2644,16 @@ var InstallEngine = class _InstallEngine {
2184
2644
  return artifacts;
2185
2645
  }
2186
2646
  async removeEmptyProjectionDirs(workspaceRoot, projPath) {
2187
- let dir = path6.dirname(path6.join(workspaceRoot, projPath));
2647
+ let dir = path9.dirname(path9.join(workspaceRoot, projPath));
2188
2648
  while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
2189
2649
  try {
2190
- const entries = await fs4.readdir(dir);
2650
+ const entries = await fs7.readdir(dir);
2191
2651
  if (entries.length > 0) break;
2192
- await fs4.rmdir(dir);
2652
+ await fs7.rmdir(dir);
2193
2653
  } catch {
2194
2654
  break;
2195
2655
  }
2196
- dir = path6.dirname(dir);
2656
+ dir = path9.dirname(dir);
2197
2657
  }
2198
2658
  }
2199
2659
  // ADR 0016: Silent auto-migration of legacy .agents/workflows/*.md into
@@ -2203,27 +2663,27 @@ var InstallEngine = class _InstallEngine {
2203
2663
  let migratedCount = 0;
2204
2664
  let targetLockfile = lockfile;
2205
2665
  let lockfileLoadedFromDisk = false;
2206
- if (!targetLockfile && await fs4.pathExists(subPaths.lockfile)) {
2666
+ if (!targetLockfile && await fs7.pathExists(subPaths.lockfile)) {
2207
2667
  try {
2208
- targetLockfile = await fs4.readJson(subPaths.lockfile);
2668
+ targetLockfile = await fs7.readJson(subPaths.lockfile);
2209
2669
  lockfileLoadedFromDisk = true;
2210
2670
  } catch {
2211
2671
  }
2212
2672
  }
2213
- if (await fs4.pathExists(subPaths.workflowsDir)) {
2214
- const entries = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
2673
+ if (await fs7.pathExists(subPaths.workflowsDir)) {
2674
+ const entries = await fs7.readdir(subPaths.workflowsDir).catch(() => []);
2215
2675
  for (const entry of entries) {
2216
2676
  if (entry.endsWith(".md")) {
2217
- const legacyPath = path6.join(subPaths.workflowsDir, entry);
2677
+ const legacyPath = path9.join(subPaths.workflowsDir, entry);
2218
2678
  let skillName = entry.replace(/\.md$/, "").replace(/--/g, "-");
2219
2679
  if (!skillName.startsWith("workflow-")) {
2220
2680
  skillName = `workflow-${skillName}`;
2221
2681
  }
2222
- const targetSkillDir = path6.join(subPaths.skillsDir, skillName);
2223
- const targetSkillFile = path6.join(targetSkillDir, "SKILL.md");
2224
- if (!await fs4.pathExists(targetSkillFile)) {
2225
- await fs4.ensureDir(targetSkillDir);
2226
- const legacyContent = await fs4.readFile(legacyPath, "utf8");
2682
+ const targetSkillDir = path9.join(subPaths.skillsDir, skillName);
2683
+ const targetSkillFile = path9.join(targetSkillDir, "SKILL.md");
2684
+ if (!await fs7.pathExists(targetSkillFile)) {
2685
+ await fs7.ensureDir(targetSkillDir);
2686
+ const legacyContent = await fs7.readFile(legacyPath, "utf8");
2227
2687
  let skillContent = legacyContent;
2228
2688
  const fmMatch = legacyContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
2229
2689
  if (fmMatch) {
@@ -2244,14 +2704,14 @@ ${YAML2.stringify(meta).trim()}
2244
2704
  } catch {
2245
2705
  }
2246
2706
  }
2247
- await fs4.writeFile(targetSkillFile, skillContent, "utf8");
2707
+ await fs7.writeFile(targetSkillFile, skillContent, "utf8");
2248
2708
  }
2249
- await fs4.remove(legacyPath).catch(() => {
2709
+ await fs7.remove(legacyPath).catch(() => {
2250
2710
  });
2251
2711
  migratedCount++;
2252
2712
  if (targetLockfile) {
2253
- const legacyRel = this.toPosix(path6.relative(targetDir, legacyPath));
2254
- const newRel = this.toPosix(path6.relative(targetDir, targetSkillFile));
2713
+ const legacyRel = this.toPosix(path9.relative(targetDir, legacyPath));
2714
+ const newRel = this.toPosix(path9.relative(targetDir, targetSkillFile));
2255
2715
  if (targetLockfile.files && targetLockfile.files[legacyRel]) {
2256
2716
  targetLockfile.files[newRel] = targetLockfile.files[legacyRel];
2257
2717
  delete targetLockfile.files[legacyRel];
@@ -2264,9 +2724,9 @@ ${YAML2.stringify(meta).trim()}
2264
2724
  }
2265
2725
  }
2266
2726
  }
2267
- const remaining = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
2727
+ const remaining = await fs7.readdir(subPaths.workflowsDir).catch(() => []);
2268
2728
  if (remaining.length === 0) {
2269
- await fs4.remove(subPaths.workflowsDir).catch(() => {
2729
+ await fs7.remove(subPaths.workflowsDir).catch(() => {
2270
2730
  });
2271
2731
  }
2272
2732
  }
@@ -2282,8 +2742,8 @@ ${YAML2.stringify(meta).trim()}
2282
2742
  }
2283
2743
  targetLockfile.installed.workflows = [];
2284
2744
  }
2285
- if (targetLockfile && (lockfileLoadedFromDisk || await fs4.pathExists(subPaths.lockfile))) {
2286
- await fs4.writeJson(subPaths.lockfile, targetLockfile, { spaces: 2 });
2745
+ if (targetLockfile && (lockfileLoadedFromDisk || await fs7.pathExists(subPaths.lockfile))) {
2746
+ await fs7.writeJson(subPaths.lockfile, targetLockfile, { spaces: 2 });
2287
2747
  }
2288
2748
  return migratedCount;
2289
2749
  }
@@ -2319,10 +2779,10 @@ ${YAML2.stringify(meta).trim()}
2319
2779
  const base = resolveHostProjectDir(host, root);
2320
2780
  const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
2321
2781
  for (const agentFile of resolved.agents) {
2322
- const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
2782
+ const content = await fs7.readFile(path9.join(registryDir, "agents", agentFile), "utf8");
2323
2783
  const res = HostProjector.projectAgent(content, HOST_REGISTRY[host].profile, this.canonicalRelAgent(agentFile));
2324
- const dest = path6.join(base, subdir, agentFile);
2325
- infos.push({ host, path: this.toPosix(path6.relative(root, dest)), kind: "role", warnings: res.warnings });
2784
+ const dest = path9.join(base, subdir, agentFile);
2785
+ infos.push({ host, path: this.toPosix(path9.relative(root, dest)), kind: "role", warnings: res.warnings });
2326
2786
  }
2327
2787
  }
2328
2788
  return infos;
@@ -2351,9 +2811,9 @@ ${YAML2.stringify(meta).trim()}
2351
2811
  resolved.workflows.forEach((f) => assets.push({ name: f.replace(/\.md$/, ""), type: "workflow", relPath: `.agents/workflows/${f}` }));
2352
2812
  resolved.skills.forEach((s) => assets.push({ name: s, type: "skill", relPath: `.agents/skills/${s}/SKILL.md` }));
2353
2813
  const bridge = HostProjector.buildAgentsMdIndex(assets);
2354
- const bridgeDest = path6.join(root, "AGENTS.md");
2355
- if (await fs4.pathExists(bridgeDest) && !options.force) {
2356
- const existing = await fs4.readFile(bridgeDest, "utf8");
2814
+ const bridgeDest = path9.join(root, "AGENTS.md");
2815
+ if (await fs7.pathExists(bridgeDest) && !options.force) {
2816
+ const existing = await fs7.readFile(bridgeDest, "utf8");
2357
2817
  if (!HostProjector.hasManagedMarker(existing)) {
2358
2818
  throw new Error(
2359
2819
  "File AGENTS.md already exists at the workspace root and is not managed by agents-united. Use --force to overwrite."
@@ -2364,7 +2824,7 @@ ${YAML2.stringify(meta).trim()}
2364
2824
  projections.push({ host, path: "AGENTS.md", kind: "bridge", warnings: [] });
2365
2825
  resolved.agents.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("agents", f), "AGENTS.md"));
2366
2826
  resolved.workflows.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("workflows", f), "AGENTS.md"));
2367
- resolved.skills.forEach((s) => this.recordProjectedTo(lockfile, this.lockRel("skills", path6.join(s, "SKILL.md")), "AGENTS.md"));
2827
+ resolved.skills.forEach((s) => this.recordProjectedTo(lockfile, this.lockRel("skills", path9.join(s, "SKILL.md")), "AGENTS.md"));
2368
2828
  continue;
2369
2829
  }
2370
2830
  if (host === "cline" || host === "claude") {
@@ -2404,8 +2864,8 @@ ${YAML2.stringify(meta).trim()}
2404
2864
  for (const artifact of parentProjection) {
2405
2865
  if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
2406
2866
  if (artifact.content === void 0) continue;
2407
- const dest = path6.join(root, artifact.relPath);
2408
- if (!await fs4.pathExists(dest)) continue;
2867
+ const dest = path9.join(root, artifact.relPath);
2868
+ if (!await fs7.pathExists(dest)) continue;
2409
2869
  await this.deployProjection(dest, artifact.content);
2410
2870
  const deployedHash = await this.calculateHash(dest);
2411
2871
  lockfile.projections = lockfile.projections || {};
@@ -2429,7 +2889,7 @@ ${YAML2.stringify(meta).trim()}
2429
2889
  const base = resolveHostProjectDir(host, root);
2430
2890
  const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
2431
2891
  for (const agentFile of resolved.agents) {
2432
- const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
2892
+ const content = await fs7.readFile(path9.join(registryDir, "agents", agentFile), "utf8");
2433
2893
  const canonicalRel = this.canonicalRelAgent(agentFile);
2434
2894
  const isCoordinatorRole = /^\s*type:\s*orchestrator\s*$/m.test(content) || /^\s*mainAgent:\s*true\s*$/m.test(content);
2435
2895
  const peerRoles = resolved.agents.filter((f) => f !== agentFile).map((f) => ClaudeProjector.stripSubagentPrefix(f.replace(/\.md$/i, "")));
@@ -2438,8 +2898,8 @@ ${YAML2.stringify(meta).trim()}
2438
2898
  }) : HostProjector.projectAgent(content, HOST_REGISTRY[host].profile, canonicalRel);
2439
2899
  const strippedRoleName = agentFile.replace(/\.md$/i, "").replace(/^subagent-/, "");
2440
2900
  const projName = host === "cline" ? `${strippedRoleName}.yml` : host === "claude" ? `${strippedRoleName}.md` : agentFile;
2441
- const dest = path6.join(base, subdir, projName);
2442
- const projPath = this.toPosix(path6.relative(root, dest));
2901
+ const dest = path9.join(base, subdir, projName);
2902
+ const projPath = this.toPosix(path9.relative(root, dest));
2443
2903
  const existingRoleProj = lockfile.projections?.[projPath];
2444
2904
  if (existingRoleProj?.kind === "role") {
2445
2905
  const compoundOwned = !(existingRoleProj.canonical ?? "").startsWith(".agents/");
@@ -2447,11 +2907,11 @@ ${YAML2.stringify(meta).trim()}
2447
2907
  continue;
2448
2908
  }
2449
2909
  }
2450
- if (await fs4.pathExists(dest) && !options.force) {
2451
- const existing = await fs4.readFile(dest, "utf8");
2910
+ if (await fs7.pathExists(dest) && !options.force) {
2911
+ const existing = await fs7.readFile(dest, "utf8");
2452
2912
  if (!HostProjector.hasManagedMarker(existing)) {
2453
2913
  throw new Error(
2454
- `Projection target ${this.toPosix(path6.relative(root, dest))} already exists and is not managed by agents-united. Use --force to overwrite.`
2914
+ `Projection target ${this.toPosix(path9.relative(root, dest))} already exists and is not managed by agents-united. Use --force to overwrite.`
2455
2915
  );
2456
2916
  }
2457
2917
  }
@@ -2486,16 +2946,16 @@ ${YAML2.stringify(meta).trim()}
2486
2946
  const hosts = this.parseHosts(options);
2487
2947
  const resolved = await this.registry.resolve(identifier);
2488
2948
  const registryDir = this.registry.getRegistryDir();
2489
- const targetDirs = hosts.map((h) => AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
2490
2949
  const hasCanonicalAgents = hosts.includes("agents");
2491
2950
  const fanoutHosts = (options.fanout || []).map((h) => typeof h === "string" ? h.trim().toLowerCase() : h).filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
2951
+ const stateDir = _InstallEngine.resolveInstallStateDir(scope, options, fanoutHosts);
2952
+ const targetDirs = hosts.map((h) => options.targetDir && !(h === "agents") ? path9.resolve(options.targetDir) : h === "agents" ? stateDir : AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
2492
2953
  if (options.dryRun) {
2493
2954
  let effectiveDryFanout = fanoutHosts;
2494
2955
  let effectiveDryPluginLane = options.pluginLane === true;
2495
- const agentsTarget2 = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
2496
- const lockfilePath = path6.join(agentsTarget2, "agents-united.json");
2497
- if (await fs4.pathExists(lockfilePath)) {
2498
- const lockfile = await fs4.readJson(lockfilePath).catch(() => null);
2956
+ const lockfilePath = path9.join(stateDir, "agents-united.json");
2957
+ if (await fs7.pathExists(lockfilePath)) {
2958
+ const lockfile = await fs7.readJson(lockfilePath).catch(() => null);
2499
2959
  if (options.fanout === void 0 && hasCanonicalAgents && lockfile?.fanout) {
2500
2960
  effectiveDryFanout = lockfile.fanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
2501
2961
  }
@@ -2503,24 +2963,28 @@ ${YAML2.stringify(meta).trim()}
2503
2963
  effectiveDryPluginLane = true;
2504
2964
  }
2505
2965
  }
2506
- const projections2 = hasCanonicalAgents ? await this.buildProjections(effectiveDryFanout, resolved, registryDir, scope, options.targetDir, effectiveDryPluginLane) : [];
2966
+ const projections2 = hasCanonicalAgents ? await this.buildProjections(effectiveDryFanout, resolved, registryDir, scope, stateDir, effectiveDryPluginLane) : [];
2507
2967
  return { installed: resolved, targetDirs, dryRun: true, method, projections: projections2 };
2508
2968
  }
2509
2969
  const now = (/* @__PURE__ */ new Date()).toISOString();
2510
2970
  const projections = [];
2511
- const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
2971
+ const agentsTarget = stateDir;
2972
+ if (hasCanonicalAgents) await _InstallEngine.upgradeSidecarToStore(stateDir);
2512
2973
  for (const targetDir of targetDirs) {
2513
2974
  const subPaths = AgentHostAdapter.getSubPaths(targetDir);
2514
- await fs4.ensureDir(subPaths.agentsDir);
2515
- await fs4.ensureDir(subPaths.skillsDir);
2975
+ const iterMethod = isSidecarDir(targetDir) ? "copy" : method;
2976
+ await fs7.ensureDir(subPaths.agentsDir);
2977
+ await fs7.ensureDir(subPaths.skillsDir);
2516
2978
  if (resolved.workflows && resolved.workflows.length > 0) {
2517
- await fs4.ensureDir(subPaths.workflowsDir);
2979
+ await fs7.ensureDir(subPaths.workflowsDir);
2518
2980
  }
2519
- await fs4.ensureDir(subPaths.rulesDir);
2981
+ await fs7.ensureDir(subPaths.rulesDir);
2520
2982
  const lockfile = await this.readLockfile(subPaths.lockfile);
2521
2983
  await this.migrateLegacyWorkspaceWorkflows(targetDir, lockfile);
2522
2984
  lockfile.scope = scope;
2523
- lockfile.method = method;
2985
+ lockfile.method = iterMethod;
2986
+ if (isSidecarDir(targetDir)) lockfile.storeShape = "sidecar";
2987
+ else delete lockfile.storeShape;
2524
2988
  lockfile.hosts = hosts;
2525
2989
  const effectiveFanout = (options.fanout !== void 0 ? fanoutHosts : lockfile.fanout ?? []).filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
2526
2990
  if (options.fanout !== void 0) {
@@ -2540,18 +3004,18 @@ ${YAML2.stringify(meta).trim()}
2540
3004
  }
2541
3005
  const declaresAsset = (assetKey) => !hasDeclaredRoster || declared.has(assetKey);
2542
3006
  for (const agentFile of resolved.agents) {
2543
- const src = path6.join(registryDir, "agents", agentFile);
2544
- const dest = path6.join(subPaths.agentsDir, agentFile);
2545
- if (await fs4.pathExists(dest) && !options.force && method !== "symlink") {
3007
+ const src = path9.join(registryDir, "agents", agentFile);
3008
+ const dest = path9.join(subPaths.agentsDir, agentFile);
3009
+ if (await fs7.pathExists(dest) && !options.force && method !== "symlink") {
2546
3010
  const existingHash = await this.calculateHash(dest).catch(() => null);
2547
- const relPath2 = path6.relative(targetDir, dest);
3011
+ const relPath2 = path9.relative(targetDir, dest);
2548
3012
  if (existingHash && lockfile.files[relPath2] && lockfile.files[relPath2].hash !== existingHash) {
2549
3013
  throw new Error(`File ${relPath2} has user modifications. Use --force to overwrite.`);
2550
3014
  }
2551
3015
  }
2552
- const actualMethod = await this.deployFile(src, dest, method, options.force);
3016
+ const actualMethod = await this.deployFile(src, dest, iterMethod, options.force);
2553
3017
  const hash = await this.calculateHash(src);
2554
- const relPath = this.toPosix(path6.relative(targetDir, dest));
3018
+ const relPath = this.toPosix(path9.relative(targetDir, dest));
2555
3019
  const existing = lockfile.files[relPath];
2556
3020
  const existingOwners = existing?.owners ?? [];
2557
3021
  const owners = resolved.targetBundle && declaresAsset(`agents/${agentFile}`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
@@ -2568,13 +3032,13 @@ ${YAML2.stringify(meta).trim()}
2568
3032
  }
2569
3033
  }
2570
3034
  for (const skillName of resolved.skills) {
2571
- const src = path6.join(registryDir, "skills", skillName);
2572
- const dest = path6.join(subPaths.skillsDir, skillName);
2573
- const actualMethod = await this.deployFile(src, dest, method, options.force);
2574
- const skillFile = path6.join(src, "SKILL.md");
2575
- if (await fs4.pathExists(skillFile)) {
3035
+ const src = path9.join(registryDir, "skills", skillName);
3036
+ const dest = path9.join(subPaths.skillsDir, skillName);
3037
+ const actualMethod = await this.deployFile(src, dest, iterMethod, options.force);
3038
+ const skillFile = path9.join(src, "SKILL.md");
3039
+ if (await fs7.pathExists(skillFile)) {
2576
3040
  const hash = await this.calculateHash(skillFile);
2577
- const relPath = this.toPosix(path6.relative(targetDir, path6.join(subPaths.skillsDir, skillName, "SKILL.md")));
3041
+ const relPath = this.toPosix(path9.relative(targetDir, path9.join(subPaths.skillsDir, skillName, "SKILL.md")));
2578
3042
  const existing = lockfile.files[relPath];
2579
3043
  const existingOwners = existing?.owners ?? [];
2580
3044
  const owners = resolved.targetBundle && declaresAsset(`skills/${skillName}/SKILL.md`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
@@ -2590,14 +3054,14 @@ ${YAML2.stringify(meta).trim()}
2590
3054
  const sidecarStack = [dest];
2591
3055
  while (sidecarStack.length > 0) {
2592
3056
  const dir = sidecarStack.pop();
2593
- for (const entry of await fs4.readdir(dir, { withFileTypes: true })) {
2594
- const abs = path6.join(dir, entry.name);
3057
+ for (const entry of await fs7.readdir(dir, { withFileTypes: true })) {
3058
+ const abs = path9.join(dir, entry.name);
2595
3059
  if (entry.isDirectory()) {
2596
3060
  sidecarStack.push(abs);
2597
3061
  continue;
2598
3062
  }
2599
- const sideRel = this.toPosix(path6.relative(targetDir, abs));
2600
- if (abs === path6.join(dest, "SKILL.md")) continue;
3063
+ const sideRel = this.toPosix(path9.relative(targetDir, abs));
3064
+ if (abs === path9.join(dest, "SKILL.md")) continue;
2601
3065
  const existingSide = lockfile.files[sideRel];
2602
3066
  const existingSideOwners = existingSide?.owners ?? [];
2603
3067
  const sideOwners = resolved.targetBundle && declaresAsset(`skills/${skillName}/SKILL.md`) ? Array.from(/* @__PURE__ */ new Set([...existingSideOwners, resolved.targetBundle])) : existingSideOwners;
@@ -2616,11 +3080,11 @@ ${YAML2.stringify(meta).trim()}
2616
3080
  }
2617
3081
  }
2618
3082
  for (const workflowFile of resolved.workflows) {
2619
- const src = path6.join(registryDir, "workflows", workflowFile);
2620
- const dest = path6.join(subPaths.workflowsDir, workflowFile);
2621
- const actualMethod = await this.deployFile(src, dest, method, options.force);
3083
+ const src = path9.join(registryDir, "workflows", workflowFile);
3084
+ const dest = path9.join(subPaths.workflowsDir, workflowFile);
3085
+ const actualMethod = await this.deployFile(src, dest, iterMethod, options.force);
2622
3086
  const hash = await this.calculateHash(src);
2623
- const relPath = this.toPosix(path6.relative(targetDir, dest));
3087
+ const relPath = this.toPosix(path9.relative(targetDir, dest));
2624
3088
  const existing = lockfile.files[relPath];
2625
3089
  const existingOwners = existing?.owners ?? [];
2626
3090
  const owners = resolved.targetBundle && declaresAsset(`workflows/${workflowFile}`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
@@ -2638,12 +3102,12 @@ ${YAML2.stringify(meta).trim()}
2638
3102
  }
2639
3103
  }
2640
3104
  for (const ruleFile of resolved.rules) {
2641
- const src = path6.join(registryDir, "rules", ruleFile);
2642
- const dest = path6.join(subPaths.rulesDir, ruleFile);
2643
- if (await fs4.pathExists(src)) {
2644
- const actualMethod = await this.deployFile(src, dest, method, options.force);
3105
+ const src = path9.join(registryDir, "rules", ruleFile);
3106
+ const dest = path9.join(subPaths.rulesDir, ruleFile);
3107
+ if (await fs7.pathExists(src)) {
3108
+ const actualMethod = await this.deployFile(src, dest, iterMethod, options.force);
2645
3109
  const hash = await this.calculateHash(src);
2646
- const relPath = this.toPosix(path6.relative(targetDir, dest));
3110
+ const relPath = this.toPosix(path9.relative(targetDir, dest));
2647
3111
  const existing = lockfile.files[relPath];
2648
3112
  const existingOwners = existing?.owners ?? [];
2649
3113
  const owners = resolved.targetBundle ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
@@ -2669,27 +3133,33 @@ ${YAML2.stringify(meta).trim()}
2669
3133
  lockfile.bundleModes[resolved.targetBundle] = options.mode;
2670
3134
  }
2671
3135
  }
2672
- if (hasCanonicalAgents && effectiveFanout.length > 0 && path6.resolve(targetDir) === path6.resolve(agentsTarget)) {
3136
+ if (hasCanonicalAgents && effectiveFanout.length > 0 && path9.resolve(targetDir) === path9.resolve(agentsTarget)) {
2673
3137
  await this.applyFanout(effectiveFanout, resolved, registryDir, scope, targetDir, lockfile, options, projections);
2674
3138
  }
2675
- await fs4.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
3139
+ if (hasCanonicalAgents && effectiveFanout.includes("claude") && path9.resolve(targetDir) === path9.resolve(agentsTarget)) {
3140
+ await this.applySessionGuard(scope, workspaceRootOf(agentsTarget), lockfile, options, projections);
3141
+ }
3142
+ if (hasCanonicalAgents && effectiveFanout.length > 0 && path9.resolve(targetDir) === path9.resolve(agentsTarget)) {
3143
+ await this.applyPermissionPreset(scope, workspaceRootOf(agentsTarget), lockfile, options, effectiveFanout, projections);
3144
+ }
3145
+ await fs7.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
2676
3146
  }
2677
3147
  return { installed: resolved, targetDirs, dryRun: false, method, projections };
2678
3148
  }
2679
3149
  };
2680
3150
 
2681
3151
  // src/core/uninstaller.ts
2682
- import path7 from "path";
2683
- import crypto2 from "crypto";
2684
- import fs5 from "fs-extra";
3152
+ import path10 from "path";
3153
+ import crypto3 from "crypto";
3154
+ import fs8 from "fs-extra";
2685
3155
  var UninstallEngine = class {
2686
3156
  registry;
2687
3157
  constructor(registry2) {
2688
3158
  this.registry = registry2 || new RegistryResolver();
2689
3159
  }
2690
3160
  async calculateHash(filePath) {
2691
- const buffer = await fs5.readFile(filePath);
2692
- return "sha256:" + crypto2.createHash("sha256").update(buffer).digest("hex");
3161
+ const buffer = await fs8.readFile(filePath);
3162
+ return "sha256:" + crypto3.createHash("sha256").update(buffer).digest("hex");
2693
3163
  }
2694
3164
  /**
2695
3165
  * Determines whether a projected file still carries our managed marker. Agent
@@ -2698,14 +3168,14 @@ var UninstallEngine = class {
2698
3168
  * frontmatter, so the marker is checked against the whole content.
2699
3169
  */
2700
3170
  async isManagedProjection(absPath, isAgentsMd, recordedHash) {
2701
- const content = await fs5.readFile(absPath, "utf8");
3171
+ const content = await fs8.readFile(absPath, "utf8");
2702
3172
  if (isAgentsMd) {
2703
3173
  return content.includes("managed-by: agents-united");
2704
3174
  }
2705
3175
  if (HostProjector.hasManagedMarker(content)) return true;
2706
3176
  if (recordedHash) {
2707
- const bytes = await fs5.readFile(absPath);
2708
- return `sha256:${crypto2.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
3177
+ const bytes = await fs8.readFile(absPath);
3178
+ return `sha256:${crypto3.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
2709
3179
  }
2710
3180
  return false;
2711
3181
  }
@@ -2714,16 +3184,16 @@ var UninstallEngine = class {
2714
3184
  * (e.g. `.claude/agents/` and then `.claude/`). Only ever removes empty dirs.
2715
3185
  */
2716
3186
  async removeEmptyProjectionDirs(workspaceRoot, projPath) {
2717
- let dir = path7.dirname(path7.join(workspaceRoot, projPath));
3187
+ let dir = path10.dirname(path10.join(workspaceRoot, projPath));
2718
3188
  while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
2719
3189
  try {
2720
- const entries = await fs5.readdir(dir);
3190
+ const entries = await fs8.readdir(dir);
2721
3191
  if (entries.length > 0) break;
2722
- await fs5.rmdir(dir);
3192
+ await fs8.rmdir(dir);
2723
3193
  } catch {
2724
3194
  break;
2725
3195
  }
2726
- dir = path7.dirname(dir);
3196
+ dir = path10.dirname(dir);
2727
3197
  }
2728
3198
  }
2729
3199
  /**
@@ -2732,8 +3202,8 @@ var UninstallEngine = class {
2732
3202
  */
2733
3203
  async removeProjections(workspaceRoot, projectedTo, force, projectionHashes) {
2734
3204
  for (const projPath of projectedTo) {
2735
- const absProjection = path7.join(workspaceRoot, projPath);
2736
- if (!await fs5.pathExists(absProjection)) {
3205
+ const absProjection = path10.join(workspaceRoot, projPath);
3206
+ if (!await fs8.pathExists(absProjection)) {
2737
3207
  continue;
2738
3208
  }
2739
3209
  const isAgentsMd = projPath === "AGENTS.md";
@@ -2749,14 +3219,14 @@ var UninstallEngine = class {
2749
3219
  }
2750
3220
  async safeRemoveProjection(absPath) {
2751
3221
  try {
2752
- const lstat = await fs5.lstat(absPath);
3222
+ const lstat = await fs8.lstat(absPath);
2753
3223
  if (lstat.isSymbolicLink()) {
2754
- await fs5.unlink(absPath);
3224
+ await fs8.unlink(absPath);
2755
3225
  return;
2756
3226
  }
2757
3227
  } catch {
2758
3228
  }
2759
- await fs5.remove(absPath);
3229
+ await fs8.remove(absPath);
2760
3230
  }
2761
3231
  /**
2762
3232
  * Installed-addon freshness (plan 003): after a child bundle (one with a
@@ -2785,9 +3255,9 @@ var UninstallEngine = class {
2785
3255
  for (const artifact of projection) {
2786
3256
  if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
2787
3257
  if (artifact.content === void 0) continue;
2788
- const dest = path7.join(workspaceRoot, artifact.relPath);
2789
- if (!await fs5.pathExists(dest)) continue;
2790
- await fs5.writeFile(dest, artifact.content, "utf8");
3258
+ const dest = path10.join(workspaceRoot, artifact.relPath);
3259
+ if (!await fs8.pathExists(dest)) continue;
3260
+ await fs8.writeFile(dest, artifact.content, "utf8");
2791
3261
  const hash = await this.calculateHash(dest);
2792
3262
  if (lockfile.projections?.[artifact.relPath]) {
2793
3263
  lockfile.projections[artifact.relPath].hash = hash;
@@ -2809,15 +3279,15 @@ var UninstallEngine = class {
2809
3279
  return options.scope || "project";
2810
3280
  }
2811
3281
  async safeRemove(targetDir, relPath) {
2812
- const fullPath = path7.join(targetDir, relPath);
2813
- if (relPath.startsWith("skills" + path7.sep) || relPath.startsWith("skills/")) {
3282
+ const fullPath = path10.join(targetDir, relPath);
3283
+ if (relPath.startsWith("skills" + path10.sep) || relPath.startsWith("skills/")) {
2814
3284
  const parts = relPath.split(/[/\\]/);
2815
3285
  if (parts.length >= 2) {
2816
- const skillDir = path7.join(targetDir, parts[0], parts[1]);
3286
+ const skillDir = path10.join(targetDir, parts[0], parts[1]);
2817
3287
  try {
2818
- const lstat = await fs5.lstat(skillDir);
3288
+ const lstat = await fs8.lstat(skillDir);
2819
3289
  if (lstat.isSymbolicLink()) {
2820
- await fs5.unlink(skillDir);
3290
+ await fs8.unlink(skillDir);
2821
3291
  return;
2822
3292
  }
2823
3293
  } catch {
@@ -2825,19 +3295,19 @@ var UninstallEngine = class {
2825
3295
  }
2826
3296
  }
2827
3297
  try {
2828
- const lstat = await fs5.lstat(fullPath);
3298
+ const lstat = await fs8.lstat(fullPath);
2829
3299
  if (lstat.isSymbolicLink()) {
2830
- await fs5.unlink(fullPath);
3300
+ await fs8.unlink(fullPath);
2831
3301
  return;
2832
3302
  }
2833
3303
  } catch {
2834
3304
  }
2835
- await fs5.remove(fullPath);
3305
+ await fs8.remove(fullPath);
2836
3306
  }
2837
3307
  async uninstall(identifier, options = {}) {
2838
3308
  const scope = this.parseScope(options);
2839
3309
  const hosts = this.parseHosts(options);
2840
- const targetDirs = hosts.map((h) => AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
3310
+ const targetDirs = hosts.map((h) => h === "agents" ? resolveStateDir(scope, options.targetDir) : AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
2841
3311
  const resolved = await this.registry.resolve(identifier).catch(() => null);
2842
3312
  const totalRemoved = [];
2843
3313
  const totalKept = [];
@@ -2845,10 +3315,10 @@ var UninstallEngine = class {
2845
3315
  const retainedOwners = /* @__PURE__ */ new Set();
2846
3316
  for (const targetDir of targetDirs) {
2847
3317
  const subPaths = AgentHostAdapter.getSubPaths(targetDir);
2848
- if (!await fs5.pathExists(subPaths.lockfile)) {
3318
+ if (!await fs8.pathExists(subPaths.lockfile)) {
2849
3319
  continue;
2850
3320
  }
2851
- const lockfile = await fs5.readJson(subPaths.lockfile);
3321
+ const lockfile = await fs8.readJson(subPaths.lockfile);
2852
3322
  const removedFiles = [];
2853
3323
  const keptFiles = [];
2854
3324
  const staleRecords = [];
@@ -2902,7 +3372,7 @@ var UninstallEngine = class {
2902
3372
  const ownsSomething = Object.values(lockfile.files).some((m) => assetOwners(m).includes(bundleName)) || Object.values(lockfile.projections || {}).some((p) => (p.owners ?? []).includes(bundleName));
2903
3373
  if (lockfile.installed.bundles.includes(bundleName) || ownsSomething) {
2904
3374
  if (!options.dryRun) {
2905
- const workspaceRoot = path7.resolve(path7.dirname(targetDir));
3375
+ const workspaceRoot = workspaceRootOf(targetDir);
2906
3376
  const ownedFiles = Object.entries(lockfile.files).filter(([, m]) => assetOwners(m).includes(bundleName));
2907
3377
  const ownedProjections = lockfile.projections ? Object.entries(lockfile.projections).filter(([p, v]) => projectionOwners(p, v).includes(bundleName)).length : 0;
2908
3378
  if (ownedFiles.length === 0 && ownedProjections === 0) {
@@ -2923,8 +3393,8 @@ var UninstallEngine = class {
2923
3393
  covering.forEach((o) => retainedOwners.add(o));
2924
3394
  continue;
2925
3395
  }
2926
- const absProjection = path7.join(workspaceRoot, projRelPath);
2927
- const existedOnDisk = await fs5.pathExists(absProjection);
3396
+ const absProjection = path10.join(workspaceRoot, projRelPath);
3397
+ const existedOnDisk = await fs8.pathExists(absProjection);
2928
3398
  if (existedOnDisk) {
2929
3399
  if (proj.managedMarker) {
2930
3400
  const managed = await this.isManagedProjection(absProjection, projRelPath === "AGENTS.md");
@@ -2969,8 +3439,8 @@ var UninstallEngine = class {
2969
3439
  if (assetMeta.projectedTo && assetMeta.projectedTo.length > 0) {
2970
3440
  await this.removeProjections(workspaceRoot, assetMeta.projectedTo, options.force, lockfile.projections);
2971
3441
  }
2972
- const fullPath = path7.join(targetDir, relPath);
2973
- if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
3442
+ const fullPath = path10.join(targetDir, relPath);
3443
+ if (await fs8.pathExists(fullPath) || await fs8.pathExists(fullPath).catch(() => false)) {
2974
3444
  if (!options.force && assetMeta.method !== "symlink") {
2975
3445
  const currentHash = await this.calculateHash(fullPath).catch(() => null);
2976
3446
  if (currentHash && currentHash !== assetMeta.hash) {
@@ -2988,10 +3458,10 @@ var UninstallEngine = class {
2988
3458
  }
2989
3459
  }
2990
3460
  for (const dir of [subPaths.agentsDir, subPaths.skillsDir, subPaths.workflowsDir, subPaths.rulesDir]) {
2991
- if (await fs5.pathExists(dir)) {
2992
- const entries = await fs5.readdir(dir).catch(() => []);
3461
+ if (await fs8.pathExists(dir)) {
3462
+ const entries = await fs8.readdir(dir).catch(() => []);
2993
3463
  if (entries.length === 0) {
2994
- await fs5.remove(dir);
3464
+ await fs8.remove(dir);
2995
3465
  }
2996
3466
  }
2997
3467
  }
@@ -3020,7 +3490,26 @@ var UninstallEngine = class {
3020
3490
  lockfile.installed.workflows = lockfile.installed.workflows.filter((w) => survival.has(`workflows/${w}`));
3021
3491
  }
3022
3492
  await this.refreshParentCoordination(bundleName, workspaceRoot, lockfile, scope);
3023
- await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
3493
+ const guard = lockfile.sessionGuard;
3494
+ if (lockfile.installed.bundles.length === 0 && guard && !("off" in guard)) {
3495
+ const guardFile = path10.isAbsolute(guard.file) ? guard.file : path10.join(workspaceRoot, guard.file);
3496
+ const outcome = await removeSessionGuard(guardFile, { createdFile: guard.createdFile });
3497
+ if (outcome === "removed" || outcome === "deleted-file") removedFiles.push(guard.file);
3498
+ delete lockfile.sessionGuard;
3499
+ }
3500
+ const preset = lockfile.permissionPreset;
3501
+ if (lockfile.installed.bundles.length === 0 && preset && !("off" in preset)) {
3502
+ const presetFile = path10.isAbsolute(preset.file) ? preset.file : path10.join(workspaceRoot, preset.file);
3503
+ const outcome = await removePermissionPreset(presetFile, preset.entries, { createdFile: preset.createdFile });
3504
+ if (outcome === "removed" || outcome === "deleted-file") removedFiles.push(preset.file);
3505
+ delete lockfile.permissionPreset;
3506
+ }
3507
+ await fs8.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
3508
+ if (isSidecarDir(targetDir) && lockfile.installed.bundles.length === 0) {
3509
+ await fs8.remove(targetDir);
3510
+ const claudeDir = path10.dirname(targetDir);
3511
+ if ((await fs8.readdir(claudeDir).catch(() => ["keep"])).length === 0) await fs8.remove(claudeDir);
3512
+ }
3024
3513
  } else {
3025
3514
  for (const [relPath, m] of Object.entries(lockfile.files)) {
3026
3515
  const owners = assetOwners(m);
@@ -3042,8 +3531,8 @@ var UninstallEngine = class {
3042
3531
  const itemTargetRelPaths = Object.keys(lockfile.files).filter((p) => p.includes(identifier));
3043
3532
  if (!options.dryRun) {
3044
3533
  for (const relPath of itemTargetRelPaths) {
3045
- const fullPath = path7.join(targetDir, relPath);
3046
- if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
3534
+ const fullPath = path10.join(targetDir, relPath);
3535
+ if (await fs8.pathExists(fullPath) || await fs8.pathExists(fullPath).catch(() => false)) {
3047
3536
  if (!options.force && lockfile.files[relPath]?.method !== "symlink") {
3048
3537
  const currentHash = await this.calculateHash(fullPath).catch(() => null);
3049
3538
  const assetMeta = lockfile.files[relPath];
@@ -3056,7 +3545,7 @@ var UninstallEngine = class {
3056
3545
  }
3057
3546
  delete lockfile.files[relPath];
3058
3547
  }
3059
- await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
3548
+ await fs8.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
3060
3549
  } else {
3061
3550
  removedFiles.push(...itemTargetRelPaths);
3062
3551
  }
@@ -3080,23 +3569,23 @@ var UninstallEngine = class {
3080
3569
  };
3081
3570
 
3082
3571
  // src/core/inventory.ts
3083
- import path8 from "path";
3084
- import os2 from "os";
3085
- import fs6 from "fs-extra";
3572
+ import path11 from "path";
3573
+ import os4 from "os";
3574
+ import fs9 from "fs-extra";
3086
3575
  var InventoryScanner = class _InventoryScanner {
3087
3576
  registry;
3088
3577
  constructor(registry2) {
3089
3578
  this.registry = registry2 || new RegistryResolver();
3090
3579
  }
3091
3580
  static formatDisplayLocation(scope, host, targetDir, fanout) {
3092
- const home = os2.homedir();
3581
+ const home = os4.homedir();
3093
3582
  const cwd = process.cwd();
3094
3583
  let prettyPath = targetDir;
3095
3584
  if (targetDir.startsWith(cwd)) {
3096
- const rel = path8.relative(cwd, targetDir);
3585
+ const rel = path11.relative(cwd, targetDir);
3097
3586
  prettyPath = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
3098
3587
  } else if (targetDir.startsWith(home)) {
3099
- const rel = path8.relative(home, targetDir);
3588
+ const rel = path11.relative(home, targetDir);
3100
3589
  prettyPath = `~/${rel.replace(/\\/g, "/")}`;
3101
3590
  }
3102
3591
  const fanoutSuffix = fanout && fanout.length > 0 ? ` + ${fanout.join(", ")}` : "";
@@ -3126,14 +3615,14 @@ var InventoryScanner = class _InventoryScanner {
3126
3615
  const candidateDirs = [];
3127
3616
  if (options.targetDir) {
3128
3617
  candidateDirs.push({
3129
- dir: path8.resolve(options.targetDir),
3618
+ dir: path11.resolve(options.targetDir),
3130
3619
  scope: options.scope || "project",
3131
3620
  host: hosts[0] || "agents"
3132
3621
  });
3133
3622
  } else {
3134
3623
  for (const scope of scopes) {
3135
3624
  for (const host of hosts) {
3136
- const dir = AgentHostAdapter.resolveHostDir(scope, host);
3625
+ const dir = host === "agents" ? resolveStateDir(scope, void 0, { cwd }) : AgentHostAdapter.resolveHostDir(scope, host);
3137
3626
  candidateDirs.push({ dir, scope, host });
3138
3627
  }
3139
3628
  }
@@ -3143,15 +3632,15 @@ var InventoryScanner = class _InventoryScanner {
3143
3632
  const standaloneRecords = [];
3144
3633
  const scannedTargetDirs = /* @__PURE__ */ new Set();
3145
3634
  const scannedLocationsMap = /* @__PURE__ */ new Map();
3146
- const home = os2.homedir();
3635
+ const home = os4.homedir();
3147
3636
  for (const scope of scopes) {
3148
- const canonicalDir = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
3637
+ const canonicalDir = resolveStateDir(scope, options.targetDir, { cwd });
3149
3638
  let displayLoc = canonicalDir;
3150
3639
  if (canonicalDir.startsWith(cwd)) {
3151
- const rel = path8.relative(cwd, canonicalDir);
3640
+ const rel = path11.relative(cwd, canonicalDir);
3152
3641
  displayLoc = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
3153
3642
  } else if (canonicalDir.startsWith(home)) {
3154
- const rel = path8.relative(home, canonicalDir);
3643
+ const rel = path11.relative(home, canonicalDir);
3155
3644
  displayLoc = `~/${rel.replace(/\\/g, "/")}`;
3156
3645
  }
3157
3646
  scannedLocationsMap.set(`${scope}:${canonicalDir}`, {
@@ -3163,14 +3652,14 @@ var InventoryScanner = class _InventoryScanner {
3163
3652
  });
3164
3653
  }
3165
3654
  for (const item of candidateDirs) {
3166
- const lockfilePath = path8.join(item.dir, "agents-united.json");
3167
- if (!await fs6.pathExists(lockfilePath)) {
3655
+ const lockfilePath = path11.join(item.dir, "agents-united.json");
3656
+ if (!await fs9.pathExists(lockfilePath)) {
3168
3657
  continue;
3169
3658
  }
3170
3659
  scannedTargetDirs.add(item.dir);
3171
3660
  let lockfile;
3172
3661
  try {
3173
- lockfile = await fs6.readJson(lockfilePath);
3662
+ lockfile = await fs9.readJson(lockfilePath);
3174
3663
  } catch {
3175
3664
  continue;
3176
3665
  }
@@ -3319,9 +3808,9 @@ var InventoryScanner = class _InventoryScanner {
3319
3808
  };
3320
3809
 
3321
3810
  // src/core/updater.ts
3322
- import path9 from "path";
3323
- import crypto3 from "crypto";
3324
- import fs7 from "fs-extra";
3811
+ import path12 from "path";
3812
+ import crypto4 from "crypto";
3813
+ import fs10 from "fs-extra";
3325
3814
  var UpdateEngine = class _UpdateEngine {
3326
3815
  registry;
3327
3816
  scanner;
@@ -3344,8 +3833,8 @@ var UpdateEngine = class _UpdateEngine {
3344
3833
  return deduplicated;
3345
3834
  }
3346
3835
  async calculateHash(filePath) {
3347
- const buffer = await fs7.readFile(filePath);
3348
- return "sha256:" + crypto3.createHash("sha256").update(buffer).digest("hex");
3836
+ const buffer = await fs10.readFile(filePath);
3837
+ return "sha256:" + crypto4.createHash("sha256").update(buffer).digest("hex");
3349
3838
  }
3350
3839
  /**
3351
3840
  * Determines whether a projected file still carries our managed marker (plan 007
@@ -3353,14 +3842,14 @@ var UpdateEngine = class _UpdateEngine {
3353
3842
  * the AGENTS.md bridge has no frontmatter so it is checked over the whole content.
3354
3843
  */
3355
3844
  async isManagedProjection(absPath, isAgentsMd, recordedHash) {
3356
- const content = await fs7.readFile(absPath, "utf8");
3845
+ const content = await fs10.readFile(absPath, "utf8");
3357
3846
  if (isAgentsMd) {
3358
3847
  return content.includes("managed-by: agents-united");
3359
3848
  }
3360
3849
  if (HostProjector.hasManagedMarker(content)) return true;
3361
3850
  if (recordedHash) {
3362
- const bytes = await fs7.readFile(absPath);
3363
- return `sha256:${crypto3.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
3851
+ const bytes = await fs10.readFile(absPath);
3852
+ return `sha256:${crypto4.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
3364
3853
  }
3365
3854
  return false;
3366
3855
  }
@@ -3369,13 +3858,13 @@ var UpdateEngine = class _UpdateEngine {
3369
3858
  * every recorded projection still carries the managed marker.
3370
3859
  */
3371
3860
  async staleProjection(record, lockfile) {
3372
- let workspaceRoot = path9.dirname(record.targetDir);
3861
+ const workspaceRoot = workspaceRootOf(record.targetDir);
3373
3862
  for (const assetMeta of Object.values(lockfile.files)) {
3374
3863
  if (assetMeta.bundle !== record.name) continue;
3375
3864
  if (!assetMeta.projectedTo || assetMeta.projectedTo.length === 0) continue;
3376
3865
  for (const projPath of assetMeta.projectedTo) {
3377
- const absProjection = path9.join(workspaceRoot, projPath);
3378
- if (await fs7.pathExists(absProjection)) {
3866
+ const absProjection = path12.join(workspaceRoot, projPath);
3867
+ if (await fs10.pathExists(absProjection)) {
3379
3868
  const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md", lockfile.projections?.[projPath]?.hash);
3380
3869
  if (!managed) {
3381
3870
  return projPath;
@@ -3390,16 +3879,16 @@ var UpdateEngine = class _UpdateEngine {
3390
3879
  static LEGACY_GENERATIVE_UI_PATH = /(^|\/)generative_ui\/SKILL\.md$/;
3391
3880
  /** Removes empty projection dirs left behind by the migration (never a non-empty one). */
3392
3881
  async removeEmptyProjectionDirs(workspaceRoot, projPath) {
3393
- let dir = path9.dirname(path9.join(workspaceRoot, projPath));
3882
+ let dir = path12.dirname(path12.join(workspaceRoot, projPath));
3394
3883
  while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
3395
3884
  try {
3396
- const entries = await fs7.readdir(dir);
3885
+ const entries = await fs10.readdir(dir);
3397
3886
  if (entries.length > 0) break;
3398
- await fs7.rmdir(dir);
3887
+ await fs10.rmdir(dir);
3399
3888
  } catch {
3400
3889
  break;
3401
3890
  }
3402
- dir = path9.dirname(dir);
3891
+ dir = path12.dirname(dir);
3403
3892
  }
3404
3893
  }
3405
3894
  /** Drops one projected path from every canonical file record (host-agnostic). */
@@ -3445,11 +3934,11 @@ var UpdateEngine = class _UpdateEngine {
3445
3934
  const isLegacy = proj.canonical === _UpdateEngine.LEGACY_GENERATIVE_UI_CANONICAL || _UpdateEngine.LEGACY_GENERATIVE_UI_PATH.test(posixPath);
3446
3935
  if (!isLegacy) continue;
3447
3936
  if (posixPath.includes(`/${normalized}/`)) continue;
3448
- const absProjection = path9.join(workspaceRoot, projRelPath);
3449
- if (await fs7.pathExists(absProjection)) {
3937
+ const absProjection = path12.join(workspaceRoot, projRelPath);
3938
+ if (await fs10.pathExists(absProjection)) {
3450
3939
  const managed = await this.isManagedProjection(absProjection, posixPath === "AGENTS.md");
3451
3940
  if (!managed) continue;
3452
- await fs7.remove(absProjection);
3941
+ await fs10.remove(absProjection);
3453
3942
  await this.removeEmptyProjectionDirs(workspaceRoot, projRelPath);
3454
3943
  }
3455
3944
  delete projections[projRelPath];
@@ -3509,8 +3998,8 @@ var UpdateEngine = class _UpdateEngine {
3509
3998
  if (options.dryRun) {
3510
3999
  const dryProjections = [];
3511
4000
  for (const record of recordsToUpdate) {
3512
- const lockfilePath = path9.join(record.targetDir, "agents-united.json");
3513
- if (await fs7.pathExists(lockfilePath)) {
4001
+ const lockfilePath = path12.join(record.targetDir, "agents-united.json");
4002
+ if (await fs10.pathExists(lockfilePath)) {
3514
4003
  const dryResult = await this.installer.install(record.name, {
3515
4004
  scope: record.scope,
3516
4005
  method: record.method,
@@ -3536,18 +4025,18 @@ var UpdateEngine = class _UpdateEngine {
3536
4025
  const skipped = [];
3537
4026
  const collectedProjections = [];
3538
4027
  for (const record of recordsToUpdate) {
3539
- const lockfilePath = path9.join(record.targetDir, "agents-united.json");
3540
- if (!await fs7.pathExists(lockfilePath)) {
4028
+ const lockfilePath = path12.join(record.targetDir, "agents-united.json");
4029
+ if (!await fs10.pathExists(lockfilePath)) {
3541
4030
  continue;
3542
4031
  }
3543
- const lockfile = await fs7.readJson(lockfilePath);
4032
+ const lockfile = await fs10.readJson(lockfilePath);
3544
4033
  if (record.method === "copy" && !options.force && lockfile.files) {
3545
4034
  let hasConflict = false;
3546
4035
  let conflictRelPath = "";
3547
4036
  for (const [relPath, assetMeta] of Object.entries(lockfile.files)) {
3548
4037
  if (assetMeta.bundle === record.name || relPath.includes(record.name)) {
3549
- const fullPath = path9.join(record.targetDir, relPath);
3550
- if (await fs7.pathExists(fullPath)) {
4038
+ const fullPath = path12.join(record.targetDir, relPath);
4039
+ if (await fs10.pathExists(fullPath)) {
3551
4040
  const currentHash = await this.calculateHash(fullPath).catch(() => null);
3552
4041
  if (currentHash && currentHash !== assetMeta.hash) {
3553
4042
  hasConflict = true;
@@ -3576,11 +4065,11 @@ var UpdateEngine = class _UpdateEngine {
3576
4065
  }
3577
4066
  }
3578
4067
  const migratedGenerativeUi = await this.migrateLegacyGenerativeUiProjections(
3579
- path9.dirname(record.targetDir),
4068
+ workspaceRootOf(record.targetDir),
3580
4069
  lockfile
3581
4070
  );
3582
4071
  if (migratedGenerativeUi.length > 0) {
3583
- await fs7.writeJson(lockfilePath, lockfile, { spaces: 2 });
4072
+ await fs10.writeJson(lockfilePath, lockfile, { spaces: 2 });
3584
4073
  }
3585
4074
  const installResult = await this.installer.install(record.name, {
3586
4075
  scope: record.scope,
@@ -3594,11 +4083,11 @@ var UpdateEngine = class _UpdateEngine {
3594
4083
  if (installResult.projections) {
3595
4084
  collectedProjections.push(...installResult.projections);
3596
4085
  }
3597
- if (await fs7.pathExists(lockfilePath)) {
3598
- const updatedLockfile = await fs7.readJson(lockfilePath);
4086
+ if (await fs10.pathExists(lockfilePath)) {
4087
+ const updatedLockfile = await fs10.readJson(lockfilePath);
3599
4088
  updatedLockfile.bundleVersions = updatedLockfile.bundleVersions || {};
3600
4089
  updatedLockfile.bundleVersions[record.name] = record.upstreamVersion;
3601
- await fs7.writeJson(lockfilePath, updatedLockfile, { spaces: 2 });
4090
+ await fs10.writeJson(lockfilePath, updatedLockfile, { spaces: 2 });
3602
4091
  if (updatedLockfile.fanout) {
3603
4092
  record.fanout = updatedLockfile.fanout;
3604
4093
  record.projections = updatedLockfile.fanout;
@@ -3625,18 +4114,18 @@ var UpdateEngine = class _UpdateEngine {
3625
4114
  };
3626
4115
 
3627
4116
  // src/core/doctor.ts
3628
- import path12 from "path";
3629
- import fs10 from "fs-extra";
3630
- import crypto4 from "crypto";
4117
+ import path16 from "path";
4118
+ import fs14 from "fs-extra";
4119
+ import crypto5 from "crypto";
3631
4120
  import YAML3 from "yaml";
3632
4121
 
3633
4122
  // src/core/claude-capabilities.ts
3634
- import path11 from "path";
3635
- import fs9 from "fs-extra";
4123
+ import path14 from "path";
4124
+ import fs12 from "fs-extra";
3636
4125
 
3637
4126
  // src/core/cline-capabilities.ts
3638
- import path10 from "path";
3639
- import fs8 from "fs-extra";
4127
+ import path13 from "path";
4128
+ import fs11 from "fs-extra";
3640
4129
  import { execFile } from "child_process";
3641
4130
  var defaultProcessRunner = (executable, args, options) => {
3642
4131
  return new Promise((resolve) => {
@@ -3693,7 +4182,7 @@ var ClineCapabilityProbe = class {
3693
4182
  return this.customResolver();
3694
4183
  }
3695
4184
  const envBin = process.env.CLINE_BIN_PATH;
3696
- if (envBin && path10.isAbsolute(envBin) && fs8.existsSync(envBin)) {
4185
+ if (envBin && path13.isAbsolute(envBin) && fs11.existsSync(envBin)) {
3697
4186
  return {
3698
4187
  executable: envBin,
3699
4188
  prefixArgs: [],
@@ -3707,25 +4196,25 @@ var ClineCapabilityProbe = class {
3707
4196
  const exeNames = ["cline.cmd", "cline.bat", "cline.exe", "cline"];
3708
4197
  for (const dir of dirs) {
3709
4198
  for (const exeName of exeNames) {
3710
- const fullPath = path10.join(dir, exeName);
3711
- if (fs8.existsSync(fullPath)) {
3712
- const adjacentWrapper1 = path10.join(dir, "node_modules", "cline", "bin", "cline");
3713
- const adjacentWrapper2 = path10.join(dir, "node_modules", "@cline", "cline", "bin", "cline");
3714
- if (fs8.existsSync(adjacentWrapper1)) {
4199
+ const fullPath = path13.join(dir, exeName);
4200
+ if (fs11.existsSync(fullPath)) {
4201
+ const adjacentWrapper1 = path13.join(dir, "node_modules", "cline", "bin", "cline");
4202
+ const adjacentWrapper2 = path13.join(dir, "node_modules", "@cline", "cline", "bin", "cline");
4203
+ if (fs11.existsSync(adjacentWrapper1)) {
3715
4204
  return {
3716
4205
  executable: process.execPath,
3717
4206
  prefixArgs: [adjacentWrapper1],
3718
4207
  source: "node-wrapper"
3719
4208
  };
3720
4209
  }
3721
- if (fs8.existsSync(adjacentWrapper2)) {
4210
+ if (fs11.existsSync(adjacentWrapper2)) {
3722
4211
  return {
3723
4212
  executable: process.execPath,
3724
4213
  prefixArgs: [adjacentWrapper2],
3725
4214
  source: "node-wrapper"
3726
4215
  };
3727
4216
  }
3728
- const ext = path10.extname(fullPath).toLowerCase();
4217
+ const ext = path13.extname(fullPath).toLowerCase();
3729
4218
  if (ext === ".cmd" || ext === ".bat") {
3730
4219
  return {
3731
4220
  executable: "cmd.exe",
@@ -3743,8 +4232,8 @@ var ClineCapabilityProbe = class {
3743
4232
  }
3744
4233
  } else {
3745
4234
  for (const dir of dirs) {
3746
- const fullPath = path10.join(dir, "cline");
3747
- if (fs8.existsSync(fullPath)) {
4235
+ const fullPath = path13.join(dir, "cline");
4236
+ if (fs11.existsSync(fullPath)) {
3748
4237
  return {
3749
4238
  executable: fullPath,
3750
4239
  prefixArgs: [],
@@ -3851,7 +4340,7 @@ var ClaudeCapabilityProbe = class {
3851
4340
  return this.customResolver();
3852
4341
  }
3853
4342
  const envBin = process.env.CLAUDE_BIN_PATH;
3854
- if (envBin && path11.isAbsolute(envBin) && fs9.existsSync(envBin)) {
4343
+ if (envBin && path14.isAbsolute(envBin) && fs12.existsSync(envBin)) {
3855
4344
  return {
3856
4345
  executable: envBin,
3857
4346
  prefixArgs: [],
@@ -3865,25 +4354,25 @@ var ClaudeCapabilityProbe = class {
3865
4354
  const exeNames = ["claude.cmd", "claude.bat", "claude.exe", "claude"];
3866
4355
  for (const dir of dirs) {
3867
4356
  for (const exeName of exeNames) {
3868
- const fullPath = path11.join(dir, exeName);
3869
- if (fs9.existsSync(fullPath)) {
3870
- const adjacentWrapper1 = path11.join(dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
3871
- const adjacentWrapper2 = path11.join(dir, "node_modules", "claude", "bin", "claude");
3872
- if (fs9.existsSync(adjacentWrapper1)) {
4357
+ const fullPath = path14.join(dir, exeName);
4358
+ if (fs12.existsSync(fullPath)) {
4359
+ const adjacentWrapper1 = path14.join(dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
4360
+ const adjacentWrapper2 = path14.join(dir, "node_modules", "claude", "bin", "claude");
4361
+ if (fs12.existsSync(adjacentWrapper1)) {
3873
4362
  return {
3874
4363
  executable: process.execPath,
3875
4364
  prefixArgs: [adjacentWrapper1],
3876
4365
  source: "node-wrapper"
3877
4366
  };
3878
4367
  }
3879
- if (fs9.existsSync(adjacentWrapper2)) {
4368
+ if (fs12.existsSync(adjacentWrapper2)) {
3880
4369
  return {
3881
4370
  executable: process.execPath,
3882
4371
  prefixArgs: [adjacentWrapper2],
3883
4372
  source: "node-wrapper"
3884
4373
  };
3885
4374
  }
3886
- const ext = path11.extname(fullPath).toLowerCase();
4375
+ const ext = path14.extname(fullPath).toLowerCase();
3887
4376
  if (ext === ".cmd" || ext === ".bat") {
3888
4377
  return {
3889
4378
  executable: "cmd.exe",
@@ -3901,8 +4390,8 @@ var ClaudeCapabilityProbe = class {
3901
4390
  }
3902
4391
  } else {
3903
4392
  for (const dir of dirs) {
3904
- const fullPath = path11.join(dir, "claude");
3905
- if (fs9.existsSync(fullPath)) {
4393
+ const fullPath = path14.join(dir, "claude");
4394
+ if (fs12.existsSync(fullPath)) {
3906
4395
  return {
3907
4396
  executable: fullPath,
3908
4397
  prefixArgs: [],
@@ -3993,83 +4482,368 @@ var ClaudeCapabilityProbe = class {
3993
4482
  }
3994
4483
  };
3995
4484
 
3996
- // src/core/doctor.ts
3997
- var DoctorEngine = class _DoctorEngine {
3998
- static async isManagedProjection(absPath, isAgentsMd, recordedHash) {
3999
- const content = await fs10.readFile(absPath, "utf8");
4000
- if (isAgentsMd) {
4001
- return content.includes("managed-by: agents-united");
4002
- }
4003
- if (HostProjector.hasManagedMarker(content)) return true;
4004
- if (recordedHash) {
4005
- const bytes = await fs10.readFile(absPath);
4006
- return `sha256:${crypto4.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
4007
- }
4008
- return false;
4009
- }
4010
- static isCompoundLaneHost(host) {
4011
- return host === "cline" || host === "claude";
4012
- }
4013
- /**
4014
- * Plan 015 §5.9 item 3 / ADR 0018 decision 2 — find the projection that now serves
4015
- * `canonical` at a *different* path and actually exists on disk. A hit turns an absent
4016
- * recorded path into self-healing staleness (`Stale projection … (superseded by …)`)
4017
- * instead of a user-deletion report.
4018
- *
4019
- * Covers every rename the lockfile can still carry: the ADR 0016 workflow slug rename
4020
- * (`.cline/workflows/implement-feature-or-fix.md` → `…/workflow-implement.md`) and the
4021
- * ADR 0018 prefix strip (`.claude/agents/subagent-<role>.md` → `.claude/agents/<role>.md`).
4022
- */
4023
- static async findSupersedingProjection(projPath, canonical, manifest, workspaceRoot) {
4024
- if (!canonical) return null;
4025
- for (const [projRelPath, proj] of Object.entries(manifest.projections ?? {})) {
4026
- if (projRelPath === projPath) continue;
4027
- if (proj.canonical !== canonical) continue;
4028
- if (!await fs10.pathExists(path12.join(workspaceRoot, projRelPath))) continue;
4029
- return { relPath: projRelPath, host: proj.host, owners: proj.owners ?? [] };
4030
- }
4031
- return null;
4032
- }
4485
+ // src/core/mcp-locations.ts
4486
+ import path15 from "path";
4487
+ import os5 from "os";
4488
+ import fs13 from "fs-extra";
4489
+ function resolveHomeDir() {
4490
+ return process.env.USERPROFILE || process.env.HOME || os5.homedir();
4491
+ }
4492
+ function resolveAppDataDir(homeDir) {
4493
+ return process.env.APPDATA || (process.platform === "darwin" ? path15.join(homeDir, "Library", "Application Support") : path15.join(homeDir, ".config"));
4494
+ }
4495
+ function isServerMap(value) {
4496
+ return !!value && typeof value === "object" && !Array.isArray(value);
4497
+ }
4498
+ var McpLocationRegistry = class _McpLocationRegistry {
4033
4499
  /**
4034
- * ADR 0017 — re-render the compound projections owned by the installed bundles so
4035
- * doctor can compare what is on disk with what the current renderer produces.
4036
- *
4037
- * Returns projection path -> acceptable content variants **for `host` only**, or `null`
4038
- * when the registry is unavailable (the freshness check is then skipped rather than
4039
- * guessed).
4040
- *
4041
- * ADR 0018 decision 11 — the renderer is dispatched by the *recorded projection's* host.
4042
- * Cline entries are diffed against `ClineProjector` output and Claude entries against
4043
- * `ClaudeProjector` output; nothing else is comparable, and the Cline path is unchanged
4044
- * because it renders exactly what it rendered before.
4045
- *
4046
- * Two variants are produced per path on purpose: the coordinator rule and team manifest
4047
- * legitimately render either with or without the installed-bundle addon exclusions
4048
- * (installer primary path vs parent-bundle refresh path). Matching *either* means the
4049
- * projection is fresh, which keeps the check false-positive free while still catching
4050
- * content that matches neither.
4500
+ * Declarative catalog of known MCP configuration files across AI tools, CLIs, extensions, and workspaces.
4051
4501
  */
4052
- static async renderProjectionVariants(owners, installedBundles, host) {
4053
- if (owners.size === 0) return null;
4054
- let resolver;
4055
- try {
4056
- resolver = new RegistryResolver();
4057
- await resolver.loadBundles();
4058
- } catch {
4059
- return null;
4060
- }
4061
- const registryDir = resolver.getRegistryDir();
4062
- const variants = /* @__PURE__ */ new Map();
4063
- for (const owner of owners) {
4064
- try {
4065
- const bundleDef = await resolver.getBundle(owner);
4066
- if (!bundleDef) continue;
4067
- const resolved = await resolver.resolve(bundleDef.name);
4068
- for (const excludeAddons of [void 0, installedBundles]) {
4069
- const artifacts = host === "cline" ? await ClineProjector.planCompoundProjection(
4070
- bundleDef,
4071
- "project",
4072
- resolved,
4502
+ static LOCATIONS = [
4503
+ // --- Google Antigravity & Gemini ---
4504
+ {
4505
+ id: "antigravity-global",
4506
+ host: "gemini",
4507
+ category: "cli",
4508
+ label: "Google Antigravity & Gemini Global Config",
4509
+ resolvePath: (_, home) => path15.join(home, ".gemini", "config", "mcp_config.json"),
4510
+ isPrimary: true
4511
+ },
4512
+ {
4513
+ id: "antigravity-system",
4514
+ host: "gemini",
4515
+ category: "desktop",
4516
+ label: "Google Antigravity System Config",
4517
+ resolvePath: (_, home) => path15.join(home, ".gemini", "antigravity", "mcp_config.json")
4518
+ },
4519
+ {
4520
+ id: "antigravity-workspace",
4521
+ host: "gemini",
4522
+ category: "workspace",
4523
+ label: "Google Antigravity Local Workspace Config",
4524
+ resolvePath: (cwd) => path15.join(cwd, ".gemini", "config", "mcp_config.json")
4525
+ },
4526
+ {
4527
+ id: "gemini-appdata",
4528
+ host: "gemini",
4529
+ category: "cli",
4530
+ label: "Google Gemini AppData Config",
4531
+ resolvePath: (_, __, appData) => path15.join(appData, "gemini", "config", "mcp_config.json")
4532
+ },
4533
+ // --- Cline CLI & VS Code Extension ---
4534
+ {
4535
+ id: "cline-cli-settings",
4536
+ host: "cline",
4537
+ category: "cli",
4538
+ label: "Cline CLI MCP Settings",
4539
+ resolvePath: (_, home) => path15.join(home, ".cline", "data", "settings", "cline_mcp_settings.json"),
4540
+ isPrimary: true
4541
+ },
4542
+ {
4543
+ id: "cline-extension-settings",
4544
+ host: "cline",
4545
+ category: "extension",
4546
+ label: "Cline VS Code Extension Global Settings",
4547
+ resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
4548
+ },
4549
+ {
4550
+ id: "cline-insiders-extension-settings",
4551
+ host: "cline",
4552
+ category: "extension",
4553
+ label: "Cline VS Code Insiders Extension Settings",
4554
+ resolvePath: (_, __, appData) => path15.join(appData, "Code - Insiders", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
4555
+ },
4556
+ {
4557
+ id: "cline-legacy-settings",
4558
+ host: "cline",
4559
+ category: "cli",
4560
+ label: "Cline Home Legacy Settings",
4561
+ resolvePath: (_, home) => path15.join(home, ".cline", "settings", "cline_mcp_settings.json")
4562
+ },
4563
+ {
4564
+ id: "cline-home-flat",
4565
+ host: "cline",
4566
+ category: "cli",
4567
+ label: "Cline Home Flat Settings",
4568
+ resolvePath: (_, home) => path15.join(home, "cline_mcp_settings.json")
4569
+ },
4570
+ {
4571
+ id: "cline-workspace-flat",
4572
+ host: "cline",
4573
+ category: "workspace",
4574
+ label: "Cline Workspace Settings",
4575
+ resolvePath: (cwd) => path15.join(cwd, "cline_mcp_settings.json")
4576
+ },
4577
+ {
4578
+ id: "cline-workspace-dot",
4579
+ host: "cline",
4580
+ category: "workspace",
4581
+ label: "Cline Workspace .cline/mcp.json",
4582
+ resolvePath: (cwd) => path15.join(cwd, ".cline", "mcp.json")
4583
+ },
4584
+ // --- Roo Code Extension ---
4585
+ {
4586
+ id: "roo-code-extension-settings",
4587
+ host: "cline",
4588
+ category: "extension",
4589
+ label: "Roo Code VS Code Extension Settings",
4590
+ resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json")
4591
+ },
4592
+ // --- Cursor IDE / CLI ---
4593
+ {
4594
+ id: "cursor-workspace",
4595
+ host: "cursor",
4596
+ category: "workspace",
4597
+ label: "Cursor Workspace Config",
4598
+ resolvePath: (cwd) => path15.join(cwd, ".cursor", "mcp.json"),
4599
+ isPrimary: true
4600
+ },
4601
+ {
4602
+ id: "cursor-global",
4603
+ host: "cursor",
4604
+ category: "desktop",
4605
+ label: "Cursor Global User Config",
4606
+ resolvePath: (_, home) => path15.join(home, ".cursor", "mcp.json")
4607
+ },
4608
+ // --- Claude Code CLI & Claude Desktop ---
4609
+ // Plan 029 A4 — Claude Code's documented MCP config paths. The project scope is `.mcp.json` at
4610
+ // the repo root; the user scope (`mcpServers`) and the local scope
4611
+ // (`projects.<abs-path>.mcpServers`) both live in `~/.claude.json`. Claude *Desktop* keeps its
4612
+ // own separate file, which is why that entry is untouched.
4613
+ // (Replaced here: `.claude/mcp.json`, `<cwd>/claude.json` and `~/claude.json` — none of them is
4614
+ // a Claude Code MCP config, and `~/claude.json` was a mis-spelling of `~/.claude.json`.)
4615
+ {
4616
+ id: "claude-project",
4617
+ host: "claude",
4618
+ category: "workspace",
4619
+ label: "Claude Code Project Config (.mcp.json)",
4620
+ resolvePath: (cwd) => path15.join(cwd, ".mcp.json"),
4621
+ isPrimary: true
4622
+ },
4623
+ {
4624
+ id: "claude-user-local",
4625
+ host: "claude",
4626
+ category: "cli",
4627
+ label: "Claude Code User & Local Config (~/.claude.json)",
4628
+ resolvePath: (_, home) => path15.join(home, ".claude.json"),
4629
+ // One file, two scopes: `mcpServers` is the user scope, `projects.<abs-path>.mcpServers` is
4630
+ // the local scope. A workspace can reach both, so discovery reports the union (a local entry
4631
+ // wins on a name collision). The project key is tried in its raw, resolved and forward-slash
4632
+ // spellings because Claude Code writes the absolute project path as the key.
4633
+ selectServers: (config, cwd) => {
4634
+ const user = isServerMap(config?.mcpServers) ? config.mcpServers : {};
4635
+ const projects = isServerMap(config?.projects) ? config.projects : {};
4636
+ const resolved = path15.resolve(cwd);
4637
+ let local = {};
4638
+ for (const key of [cwd, resolved, resolved.split(path15.sep).join("/")]) {
4639
+ const entry = projects[key];
4640
+ if (isServerMap(entry?.mcpServers)) {
4641
+ local = entry.mcpServers;
4642
+ break;
4643
+ }
4644
+ }
4645
+ const merged = { ...user, ...local };
4646
+ return Object.keys(merged).length > 0 ? merged : void 0;
4647
+ }
4648
+ },
4649
+ {
4650
+ id: "claude-desktop-global",
4651
+ host: "claude",
4652
+ category: "desktop",
4653
+ label: "Claude Desktop Global Config",
4654
+ resolvePath: (_, __, appData) => path15.join(appData, "Claude", "claude_desktop_config.json")
4655
+ },
4656
+ // --- Windsurf IDE ---
4657
+ {
4658
+ id: "windsurf-global",
4659
+ host: "windsurf",
4660
+ category: "desktop",
4661
+ label: "Windsurf Global Config",
4662
+ resolvePath: (_, home) => path15.join(home, ".codeium", "windsurf", "mcp_config.json"),
4663
+ isPrimary: true
4664
+ },
4665
+ {
4666
+ id: "windsurf-appdata",
4667
+ host: "windsurf",
4668
+ category: "desktop",
4669
+ label: "Windsurf AppData Config",
4670
+ resolvePath: (_, __, appData) => path15.join(appData, "Windsurf", "mcp_config.json")
4671
+ },
4672
+ // --- OpenCode & Codex ---
4673
+ {
4674
+ id: "opencode-workspace",
4675
+ host: "opencode",
4676
+ category: "workspace",
4677
+ label: "OpenCode Workspace Config",
4678
+ resolvePath: (cwd) => path15.join(cwd, ".opencode", "mcp.json"),
4679
+ isPrimary: true
4680
+ },
4681
+ {
4682
+ id: "opencode-global",
4683
+ host: "opencode",
4684
+ category: "cli",
4685
+ label: "OpenCode Global Config",
4686
+ resolvePath: (_, home) => path15.join(home, ".config", "opencode", "mcp.json")
4687
+ }
4688
+ ];
4689
+ /**
4690
+ * Discovers all MCP configuration files across the filesystem, inspecting existence and parsing server definitions.
4691
+ */
4692
+ static async discoverAll(cwd = process.cwd()) {
4693
+ const homeDir = resolveHomeDir();
4694
+ const appData = resolveAppDataDir(homeDir);
4695
+ const results = [];
4696
+ const seenPaths = /* @__PURE__ */ new Set();
4697
+ for (const desc of _McpLocationRegistry.LOCATIONS) {
4698
+ const resolved = path15.normalize(desc.resolvePath(cwd, homeDir, appData));
4699
+ const key = resolved.toLowerCase();
4700
+ if (seenPaths.has(key)) continue;
4701
+ seenPaths.add(key);
4702
+ const exists = await fs13.pathExists(resolved);
4703
+ let serverCount = 0;
4704
+ let servers;
4705
+ if (exists) {
4706
+ try {
4707
+ const config = await fs13.readJson(resolved);
4708
+ const rawServers = desc.selectServers ? desc.selectServers(config, cwd) : config?.mcpServers || config?.servers || config;
4709
+ if (rawServers && typeof rawServers === "object") {
4710
+ servers = rawServers;
4711
+ serverCount = Object.keys(rawServers).length;
4712
+ }
4713
+ } catch {
4714
+ }
4715
+ }
4716
+ results.push({
4717
+ id: desc.id,
4718
+ host: desc.host,
4719
+ category: desc.category,
4720
+ label: desc.label,
4721
+ path: resolved,
4722
+ exists,
4723
+ serverCount,
4724
+ servers,
4725
+ isPrimary: desc.isPrimary
4726
+ });
4727
+ }
4728
+ return results;
4729
+ }
4730
+ /**
4731
+ * Returns discovered locations filtered for specific host(s).
4732
+ */
4733
+ static async discoverForHosts(hosts, cwd = process.cwd()) {
4734
+ const all = await _McpLocationRegistry.discoverAll(cwd);
4735
+ const hostSet = new Set(hosts.map((h) => h === "agents" ? "gemini" : h));
4736
+ return all.filter((loc) => hostSet.has(loc.host));
4737
+ }
4738
+ /**
4739
+ * Resolves the primary target write path for a given host runtime.
4740
+ */
4741
+ static getPrimaryWritePath(host, cwd = process.cwd()) {
4742
+ const homeDir = resolveHomeDir();
4743
+ const appData = resolveAppDataDir(homeDir);
4744
+ const targetHost = host === "agents" ? "gemini" : host;
4745
+ const desc = _McpLocationRegistry.LOCATIONS.find((l) => l.host === targetHost && l.isPrimary);
4746
+ if (desc) {
4747
+ return path15.normalize(desc.resolvePath(cwd, homeDir, appData));
4748
+ }
4749
+ return path15.normalize(path15.join(cwd, ".cursor", "mcp.json"));
4750
+ }
4751
+ };
4752
+
4753
+ // src/core/doctor.ts
4754
+ var DECLARED_DELTA_HOSTS = /* @__PURE__ */ new Set(["claude", "antigravity", "cline"]);
4755
+ var MCP_AUDIT_HOSTS = /* @__PURE__ */ new Set(["claude", "antigravity", "cline"]);
4756
+ var MCP_LOCATION_HOST = {
4757
+ claude: "claude",
4758
+ antigravity: "gemini",
4759
+ cline: "cline"
4760
+ };
4761
+ function mcpAddCommand(host, server, workspaceRoot) {
4762
+ if (host === "antigravity") {
4763
+ return `Add it with: agy mcp add ${server} -- <command> [args...]`;
4764
+ }
4765
+ if (host === "cline") {
4766
+ const settingsPath = McpLocationRegistry.getPrimaryWritePath("cline", workspaceRoot);
4767
+ return `Add it with the interactive wizard: cline mcp install|add <name> (or cline config mcp for the current list); settings live in ${settingsPath}.`;
4768
+ }
4769
+ return `Add it with: claude mcp add ${server} -- <command> [args...]`;
4770
+ }
4771
+ var DoctorEngine = class _DoctorEngine {
4772
+ static async isManagedProjection(absPath, isAgentsMd, recordedHash) {
4773
+ const content = await fs14.readFile(absPath, "utf8");
4774
+ if (isAgentsMd) {
4775
+ return content.includes("managed-by: agents-united");
4776
+ }
4777
+ if (HostProjector.hasManagedMarker(content)) return true;
4778
+ if (recordedHash) {
4779
+ const bytes = await fs14.readFile(absPath);
4780
+ return `sha256:${crypto5.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
4781
+ }
4782
+ return false;
4783
+ }
4784
+ static isCompoundLaneHost(host) {
4785
+ return host === "cline" || host === "claude";
4786
+ }
4787
+ /**
4788
+ * Plan 015 §5.9 item 3 / ADR 0018 decision 2 — find the projection that now serves
4789
+ * `canonical` at a *different* path and actually exists on disk. A hit turns an absent
4790
+ * recorded path into self-healing staleness (`Stale projection … (superseded by …)`)
4791
+ * instead of a user-deletion report.
4792
+ *
4793
+ * Covers every rename the lockfile can still carry: the ADR 0016 workflow slug rename
4794
+ * (`.cline/workflows/implement-feature-or-fix.md` → `…/workflow-implement.md`) and the
4795
+ * ADR 0018 prefix strip (`.claude/agents/subagent-<role>.md` → `.claude/agents/<role>.md`).
4796
+ */
4797
+ static async findSupersedingProjection(projPath, canonical, manifest, workspaceRoot) {
4798
+ if (!canonical) return null;
4799
+ for (const [projRelPath, proj] of Object.entries(manifest.projections ?? {})) {
4800
+ if (projRelPath === projPath) continue;
4801
+ if (proj.canonical !== canonical) continue;
4802
+ if (!await fs14.pathExists(path16.join(workspaceRoot, projRelPath))) continue;
4803
+ return { relPath: projRelPath, host: proj.host, owners: proj.owners ?? [] };
4804
+ }
4805
+ return null;
4806
+ }
4807
+ /**
4808
+ * ADR 0017 — re-render the compound projections owned by the installed bundles so
4809
+ * doctor can compare what is on disk with what the current renderer produces.
4810
+ *
4811
+ * Returns projection path -> acceptable content variants **for `host` only**, or `null`
4812
+ * when the registry is unavailable (the freshness check is then skipped rather than
4813
+ * guessed).
4814
+ *
4815
+ * ADR 0018 decision 11 — the renderer is dispatched by the *recorded projection's* host.
4816
+ * Cline entries are diffed against `ClineProjector` output and Claude entries against
4817
+ * `ClaudeProjector` output; nothing else is comparable, and the Cline path is unchanged
4818
+ * because it renders exactly what it rendered before.
4819
+ *
4820
+ * Two variants are produced per path on purpose: the coordinator rule and team manifest
4821
+ * legitimately render either with or without the installed-bundle addon exclusions
4822
+ * (installer primary path vs parent-bundle refresh path). Matching *either* means the
4823
+ * projection is fresh, which keeps the check false-positive free while still catching
4824
+ * content that matches neither.
4825
+ */
4826
+ static async renderProjectionVariants(owners, installedBundles, host) {
4827
+ if (owners.size === 0) return null;
4828
+ let resolver;
4829
+ try {
4830
+ resolver = new RegistryResolver();
4831
+ await resolver.loadBundles();
4832
+ } catch {
4833
+ return null;
4834
+ }
4835
+ const registryDir = resolver.getRegistryDir();
4836
+ const variants = /* @__PURE__ */ new Map();
4837
+ for (const owner of owners) {
4838
+ try {
4839
+ const bundleDef = await resolver.getBundle(owner);
4840
+ if (!bundleDef) continue;
4841
+ const resolved = await resolver.resolve(bundleDef.name);
4842
+ for (const excludeAddons of [void 0, installedBundles]) {
4843
+ const artifacts = host === "cline" ? await ClineProjector.planCompoundProjection(
4844
+ bundleDef,
4845
+ "project",
4846
+ resolved,
4073
4847
  registryDir,
4074
4848
  excludeAddons
4075
4849
  ) : await ClaudeProjector.planCompoundProjection(
@@ -4090,10 +4864,62 @@ var DoctorEngine = class _DoctorEngine {
4090
4864
  continue;
4091
4865
  }
4092
4866
  }
4093
- return variants;
4867
+ return variants;
4868
+ }
4869
+ /**
4870
+ * Plan 029 A3 — the servers the INSTALLED roles declare, read from each state-dir agent's
4871
+ * `mcpServers:` frontmatter. Names only: a credential, an `env` block or an inline server
4872
+ * definition never reaches this set (Risk R4).
4873
+ */
4874
+ static async declaredMcpServers(agentsDir) {
4875
+ const names = /* @__PURE__ */ new Set();
4876
+ if (!await fs14.pathExists(agentsDir)) return [];
4877
+ for (const file of (await fs14.readdir(agentsDir)).filter((f) => f.endsWith(".md"))) {
4878
+ const content = await fs14.readFile(path16.join(agentsDir, file), "utf8").catch(() => null);
4879
+ if (content === null) continue;
4880
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
4881
+ if (!match) continue;
4882
+ let meta;
4883
+ try {
4884
+ meta = YAML3.parse(match[1]) ?? void 0;
4885
+ } catch {
4886
+ continue;
4887
+ }
4888
+ const entries = Array.isArray(meta?.mcpServers) ? meta?.mcpServers : [];
4889
+ for (const entry of entries) {
4890
+ const name = typeof entry === "string" ? entry : entry?.name;
4891
+ if (typeof name === "string" && name.trim().length > 0) names.add(name.trim());
4892
+ }
4893
+ }
4894
+ return [...names].sort();
4895
+ }
4896
+ /**
4897
+ * Plan 029 A3 — warn for every server an installed role declares that this host has NOT
4898
+ * configured, printing that host's own add command. STRICTLY READ-ONLY: doctor reads the MCP
4899
+ * location registry, never installs a server and never writes a config file (the printed command
4900
+ * belongs to the user, and is never executed here).
4901
+ */
4902
+ static async auditMcpServers(host, workspaceRoot, agentsDir, warnings) {
4903
+ const declared = await _DoctorEngine.declaredMcpServers(agentsDir);
4904
+ if (declared.length === 0) return [];
4905
+ const discovered = await McpLocationRegistry.discoverForHosts(
4906
+ [MCP_LOCATION_HOST[host]],
4907
+ workspaceRoot
4908
+ );
4909
+ const configured = /* @__PURE__ */ new Set();
4910
+ for (const location of discovered) {
4911
+ for (const server of Object.keys(location.servers ?? {})) configured.add(server.toLowerCase());
4912
+ }
4913
+ const unconfigured = declared.filter((server) => !configured.has(server.toLowerCase()));
4914
+ for (const server of unconfigured) {
4915
+ warnings.push(
4916
+ `MCP server "${server}" is declared by the installed roles but not configured for --host ${host}. ${mcpAddCommand(host, server, workspaceRoot)}`
4917
+ );
4918
+ }
4919
+ return unconfigured;
4094
4920
  }
4095
4921
  static async runDoctor(targetDir, host) {
4096
- const root = AgentHostAdapter.resolveHostDir("project", "agents", targetDir);
4922
+ const root = resolveStateDir("project", targetDir);
4097
4923
  const subPaths = AgentHostAdapter.getSubPaths(root);
4098
4924
  const issues = [];
4099
4925
  const warnings = [];
@@ -4104,11 +4930,11 @@ var DoctorEngine = class _DoctorEngine {
4104
4930
  let clineCapability;
4105
4931
  let claudeCapability;
4106
4932
  let isInitialized = false;
4107
- const lockfileExists = await fs10.pathExists(subPaths.lockfile);
4108
- const agentsDirExists = await fs10.pathExists(subPaths.agentsDir);
4933
+ const lockfileExists = await fs14.pathExists(subPaths.lockfile);
4934
+ const agentsDirExists = await fs14.pathExists(subPaths.agentsDir);
4109
4935
  if (lockfileExists) {
4110
4936
  try {
4111
- const parsed = await fs10.readJson(subPaths.lockfile);
4937
+ const parsed = await fs14.readJson(subPaths.lockfile);
4112
4938
  manifest = parsed;
4113
4939
  agentsCount = parsed.installed?.agents?.length || 0;
4114
4940
  skillsCount = parsed.installed?.skills?.length || 0;
@@ -4122,11 +4948,11 @@ var DoctorEngine = class _DoctorEngine {
4122
4948
  } else if (agentsDirExists) {
4123
4949
  warnings.push(`No lockfile found at ${subPaths.lockfile}, but agent files were detected in ${subPaths.agentsDir}. Run 'agents update' to sync.`);
4124
4950
  }
4125
- if (await fs10.pathExists(subPaths.agentsDir)) {
4126
- const agentFiles = await fs10.readdir(subPaths.agentsDir);
4951
+ if (await fs14.pathExists(subPaths.agentsDir)) {
4952
+ const agentFiles = await fs14.readdir(subPaths.agentsDir);
4127
4953
  for (const file of agentFiles) {
4128
4954
  if (file.endsWith(".md")) {
4129
- const content = await fs10.readFile(path12.join(subPaths.agentsDir, file), "utf8");
4955
+ const content = await fs14.readFile(path16.join(subPaths.agentsDir, file), "utf8");
4130
4956
  const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
4131
4957
  if (!frontmatterMatch) {
4132
4958
  warnings.push(`Agent ${file} is missing YAML frontmatter.`);
@@ -4143,14 +4969,14 @@ var DoctorEngine = class _DoctorEngine {
4143
4969
  }
4144
4970
  }
4145
4971
  }
4146
- if (await fs10.pathExists(subPaths.workflowsDir)) {
4147
- const remainingWorkflowFiles = (await fs10.readdir(subPaths.workflowsDir)).filter((f) => f.endsWith(".md"));
4972
+ if (await fs14.pathExists(subPaths.workflowsDir)) {
4973
+ const remainingWorkflowFiles = (await fs14.readdir(subPaths.workflowsDir)).filter((f) => f.endsWith(".md"));
4148
4974
  if (remainingWorkflowFiles.length > 0) {
4149
4975
  warnings.push(`Detected ${remainingWorkflowFiles.length} legacy workflows in ${subPaths.workflowsDir}. Run 'agents update' to automatically migrate them to modern skills.`);
4150
4976
  }
4151
4977
  }
4152
4978
  if (manifest) {
4153
- const workspaceRoot = path12.resolve(path12.dirname(root));
4979
+ const workspaceRoot = workspaceRootOf(root);
4154
4980
  const warnedProjectionPaths = /* @__PURE__ */ new Set();
4155
4981
  const pushProjectionWarning = (projPath, message) => {
4156
4982
  if (warnedProjectionPaths.has(projPath)) return;
@@ -4161,8 +4987,8 @@ var DoctorEngine = class _DoctorEngine {
4161
4987
  const projectedTo = assetMeta.projectedTo;
4162
4988
  if (!projectedTo || projectedTo.length === 0) continue;
4163
4989
  for (const projPath of projectedTo) {
4164
- const absProjection = path12.join(workspaceRoot, projPath);
4165
- if (!await fs10.pathExists(absProjection)) {
4990
+ const absProjection = path16.join(workspaceRoot, projPath);
4991
+ if (!await fs14.pathExists(absProjection)) {
4166
4992
  const superseding = await this.findSupersedingProjection(projPath, relPath, manifest, workspaceRoot);
4167
4993
  if (superseding) {
4168
4994
  const owner = superseding.owners[0];
@@ -4186,8 +5012,8 @@ var DoctorEngine = class _DoctorEngine {
4186
5012
  }
4187
5013
  if (manifest.projections) {
4188
5014
  for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
4189
- const absPath = path12.join(workspaceRoot, projRelPath);
4190
- if (!await fs10.pathExists(absPath)) {
5015
+ const absPath = path16.join(workspaceRoot, projRelPath);
5016
+ if (!await fs14.pathExists(absPath)) {
4191
5017
  const superseding = await this.findSupersedingProjection(projRelPath, proj.canonical, manifest, workspaceRoot);
4192
5018
  if (superseding) {
4193
5019
  const owner = superseding.owners[0];
@@ -4230,13 +5056,13 @@ var DoctorEngine = class _DoctorEngine {
4230
5056
  if (!_DoctorEngine.isCompoundLaneHost(projHost)) continue;
4231
5057
  const expectedVariants = variantsByHost.get(projHost);
4232
5058
  if (!expectedVariants) continue;
4233
- const absPath = path12.join(workspaceRoot, projRelPath);
4234
- if (!await fs10.pathExists(absPath)) continue;
5059
+ const absPath = path16.join(workspaceRoot, projRelPath);
5060
+ if (!await fs14.pathExists(absPath)) continue;
4235
5061
  if (!proj.managedMarker) continue;
4236
- const content = await fs10.readFile(absPath, "utf8");
5062
+ const content = await fs14.readFile(absPath, "utf8");
4237
5063
  if (!HostProjector.hasManagedMarker(content)) continue;
4238
5064
  if (proj.hash) {
4239
- const diskHash = `sha256:${crypto4.createHash("sha256").update(await fs10.readFile(absPath)).digest("hex")}`;
5065
+ const diskHash = `sha256:${crypto5.createHash("sha256").update(await fs14.readFile(absPath)).digest("hex")}`;
4240
5066
  if (diskHash !== proj.hash) {
4241
5067
  const owner = proj.owners[0];
4242
5068
  pushProjectionWarning(
@@ -4284,6 +5110,77 @@ var DoctorEngine = class _DoctorEngine {
4284
5110
  }
4285
5111
  }
4286
5112
  }
5113
+ if (manifest && isSidecarDir(root)) {
5114
+ for (const [relPath, meta] of Object.entries(manifest.files)) {
5115
+ const abs = path16.join(root, relPath);
5116
+ if (!meta.hash || !await fs14.pathExists(abs)) continue;
5117
+ const diskHash = `sha256:${crypto5.createHash("sha256").update(await fs14.readFile(abs)).digest("hex")}`;
5118
+ if (diskHash !== meta.hash) {
5119
+ warnings.push(`Sidecar snapshot modified: ${relPath} \u2014 .claude/.agents-united/ is machine-owned. Run 'agents update --force' to restore it.`);
5120
+ }
5121
+ }
5122
+ }
5123
+ let sessionGuard;
5124
+ const guardRecord = manifest?.sessionGuard;
5125
+ if (guardRecord) {
5126
+ if ("off" in guardRecord) {
5127
+ sessionGuard = "off";
5128
+ } else {
5129
+ const guardFile = path16.isAbsolute(guardRecord.file) ? guardRecord.file : path16.join(workspaceRootOf(root), guardRecord.file);
5130
+ const state = await inspectSessionGuard(guardFile);
5131
+ sessionGuard = state === "absent" ? "missing" : state;
5132
+ if (state === "missing" || state === "absent") {
5133
+ warnings.push(`Session guard missing from ${guardRecord.file}: plain Claude sessions are unguarded. Run 'agents update' to restore it.`);
5134
+ } else if (state === "modified") {
5135
+ warnings.push(`Session guard in ${guardRecord.file} was edited by hand; agents-united leaves it as-is.`);
5136
+ } else if (state === "skipped-invalid") {
5137
+ warnings.push(`Session guard not wired: ${guardRecord.file} is not valid JSON (comments or trailing commas?), so agents-united left it untouched. Paste this into it by hand:
5138
+ ${sessionGuardSnippet()}`);
5139
+ }
5140
+ }
5141
+ }
5142
+ let permissionPreset;
5143
+ const presetRecord = manifest?.permissionPreset;
5144
+ if (presetRecord) {
5145
+ if ("off" in presetRecord) {
5146
+ permissionPreset = "off";
5147
+ } else {
5148
+ const presetFile = path16.isAbsolute(presetRecord.file) ? presetRecord.file : path16.join(workspaceRootOf(root), presetRecord.file);
5149
+ if (!await fs14.pathExists(presetFile)) {
5150
+ permissionPreset = "missing";
5151
+ warnings.push(`Permission preset missing: ${presetRecord.file} not found. Run 'agents update' to restore it.`);
5152
+ } else {
5153
+ const text2 = await fs14.readFile(presetFile, "utf8").catch(() => null);
5154
+ let parsed = null;
5155
+ try {
5156
+ parsed = text2 === null ? null : JSON.parse(text2);
5157
+ } catch {
5158
+ parsed = null;
5159
+ }
5160
+ if (parsed === null) {
5161
+ permissionPreset = "skipped-invalid";
5162
+ warnings.push(`Permission preset not wired: ${presetRecord.file} is not valid JSON (comments or trailing commas?), so agents-united left it untouched.`);
5163
+ } else {
5164
+ const allow = parsed.permissions?.allow ?? [];
5165
+ const stillPresent = presetRecord.entries.every((e) => allow.includes(e));
5166
+ permissionPreset = stillPresent ? "wired" : "missing";
5167
+ if (!stillPresent) {
5168
+ warnings.push(`Permission preset partially removed from ${presetRecord.file}. Run 'agents update' to restore it, or --no-permission-preset to drop it for good.`);
5169
+ }
5170
+ }
5171
+ }
5172
+ }
5173
+ }
5174
+ let declaredDeltas;
5175
+ if (host && DECLARED_DELTA_HOSTS.has(host)) {
5176
+ const ledger = loadTranslationLedger(new RegistryResolver().getRegistryDir());
5177
+ declaredDeltas = ledger.filter(
5178
+ (e) => e.host === host && (e.disposition === "degraded" || e.disposition === "unsupported")
5179
+ );
5180
+ }
5181
+ if (host && MCP_AUDIT_HOSTS.has(host)) {
5182
+ await _DoctorEngine.auditMcpServers(host, workspaceRootOf(root), subPaths.agentsDir, warnings);
5183
+ }
4287
5184
  if (host === "cline") {
4288
5185
  const probe = new ClineCapabilityProbe();
4289
5186
  clineCapability = await probe.probe();
@@ -4306,17 +5203,20 @@ var DoctorEngine = class _DoctorEngine {
4306
5203
  agentsCount,
4307
5204
  skillsCount,
4308
5205
  workflowsCount,
5206
+ permissionPreset,
4309
5207
  clineCapability,
4310
- claudeCapability
5208
+ claudeCapability,
5209
+ sessionGuard,
5210
+ declaredDeltas
4311
5211
  };
4312
5212
  }
4313
5213
  };
4314
5214
 
4315
5215
  // src/core/cline-launcher.ts
4316
- import path13 from "path";
4317
- import os3 from "os";
4318
- import crypto5 from "crypto";
4319
- import fs11 from "fs-extra";
5216
+ import path17 from "path";
5217
+ import os6 from "os";
5218
+ import crypto6 from "crypto";
5219
+ import fs15 from "fs-extra";
4320
5220
  var ClineLauncher = class _ClineLauncher {
4321
5221
  probe;
4322
5222
  runner;
@@ -4329,7 +5229,7 @@ var ClineLauncher = class _ClineLauncher {
4329
5229
  */
4330
5230
  static generateTeamName(bundleName, workspace) {
4331
5231
  const sanitizedBundle = bundleName.replace(/[^A-Za-z0-9_-]/g, "-");
4332
- const hash = crypto5.createHash("sha256").update(path13.resolve(workspace)).digest("hex").slice(0, 8);
5232
+ const hash = crypto6.createHash("sha256").update(path17.resolve(workspace)).digest("hex").slice(0, 8);
4333
5233
  const prefix = `au-${sanitizedBundle}`;
4334
5234
  const maxPrefixLen = 64 - 9;
4335
5235
  const truncatedPrefix = prefix.slice(0, maxPrefixLen);
@@ -4353,19 +5253,19 @@ var ClineLauncher = class _ClineLauncher {
4353
5253
  if (isGlobal) {
4354
5254
  targetDir = AgentHostAdapter.resolveHostDir("global", "agents");
4355
5255
  scope = "global";
4356
- workspace = os3.homedir();
5256
+ workspace = os6.homedir();
4357
5257
  } else {
4358
5258
  workspace = cwd;
4359
- targetDir = path13.join(cwd, ".agents");
5259
+ targetDir = path17.join(cwd, ".agents");
4360
5260
  scope = "project";
4361
5261
  }
4362
- const lockfilePath = path13.join(targetDir, "agents-united.json");
4363
- if (!await fs11.pathExists(lockfilePath)) {
5262
+ const lockfilePath = path17.join(targetDir, "agents-united.json");
5263
+ if (!await fs15.pathExists(lockfilePath)) {
4364
5264
  if (!isGlobal) {
4365
5265
  const globalTarget = AgentHostAdapter.resolveHostDir("global", "agents");
4366
- const globalLockfile = path13.join(globalTarget, "agents-united.json");
4367
- if (await fs11.pathExists(globalLockfile)) {
4368
- const gLock = await fs11.readJson(globalLockfile);
5266
+ const globalLockfile = path17.join(globalTarget, "agents-united.json");
5267
+ if (await fs15.pathExists(globalLockfile)) {
5268
+ const gLock = await fs15.readJson(globalLockfile);
4369
5269
  if (gLock.installed?.bundles?.includes(bundleName)) {
4370
5270
  return this.resolveInstallation(bundleName, { ...options, global: true });
4371
5271
  }
@@ -4373,14 +5273,14 @@ var ClineLauncher = class _ClineLauncher {
4373
5273
  }
4374
5274
  throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
4375
5275
  }
4376
- const lockfile = await fs11.readJson(lockfilePath);
5276
+ const lockfile = await fs15.readJson(lockfilePath);
4377
5277
  if (!lockfile.installed?.bundles?.includes(bundleName)) {
4378
5278
  throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
4379
5279
  }
4380
5280
  const hasClineFanout = (lockfile.fanout || []).includes("cline");
4381
5281
  const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
4382
- const manifestPath = path13.join(workspace, manifestRel);
4383
- if (!hasClineFanout || !await fs11.pathExists(manifestPath)) {
5282
+ const manifestPath = path17.join(workspace, manifestRel);
5283
+ if (!hasClineFanout || !await fs15.pathExists(manifestPath)) {
4384
5284
  throw new Error(
4385
5285
  `Bundle '${bundleName}' is not projected to Cline. Run 'agents update ${bundleName} --fanout cline' first.`
4386
5286
  );
@@ -4480,9 +5380,9 @@ var ClineLauncher = class _ClineLauncher {
4480
5380
  };
4481
5381
 
4482
5382
  // src/core/claude-launcher.ts
4483
- import path14 from "path";
4484
- import os4 from "os";
4485
- import fs12 from "fs-extra";
5383
+ import path18 from "path";
5384
+ import os7 from "os";
5385
+ import fs16 from "fs-extra";
4486
5386
  var DEFAULT_COORDINATOR = "orchestrator-engineering.md";
4487
5387
  var ClaudeLauncher = class _ClaudeLauncher {
4488
5388
  probe;
@@ -4501,7 +5401,7 @@ var ClaudeLauncher = class _ClaudeLauncher {
4501
5401
  */
4502
5402
  static resolveCoordinatorName(orchestrator) {
4503
5403
  const file = (orchestrator && orchestrator.trim().length > 0 ? orchestrator.trim() : DEFAULT_COORDINATOR).replace(/\.md$/i, "");
4504
- return ClaudeProjector.stripSubagentPrefix(path14.basename(file));
5404
+ return ClaudeProjector.stripSubagentPrefix(path18.basename(file));
4505
5405
  }
4506
5406
  /**
4507
5407
  * Resolve an installed bundle for Claude Code activation. Mirrors `ClineLauncher.resolveInstallation`:
@@ -4514,21 +5414,21 @@ var ClaudeLauncher = class _ClaudeLauncher {
4514
5414
  let scope;
4515
5415
  let workspace;
4516
5416
  if (isGlobal) {
4517
- targetDir = AgentHostAdapter.resolveHostDir("global", "agents");
5417
+ targetDir = resolveStateDir("global");
4518
5418
  scope = "global";
4519
- workspace = os4.homedir();
5419
+ workspace = os7.homedir();
4520
5420
  } else {
4521
5421
  workspace = cwd;
4522
- targetDir = path14.join(cwd, ".agents");
5422
+ targetDir = resolveStateDir("project", void 0, { cwd });
4523
5423
  scope = "project";
4524
5424
  }
4525
- const lockfilePath = path14.join(targetDir, "agents-united.json");
4526
- if (!await fs12.pathExists(lockfilePath)) {
5425
+ const lockfilePath = path18.join(targetDir, "agents-united.json");
5426
+ if (!await fs16.pathExists(lockfilePath)) {
4527
5427
  if (!isGlobal) {
4528
- const globalTarget = AgentHostAdapter.resolveHostDir("global", "agents");
4529
- const globalLockfile = path14.join(globalTarget, "agents-united.json");
4530
- if (await fs12.pathExists(globalLockfile)) {
4531
- const gLock = await fs12.readJson(globalLockfile);
5428
+ const globalTarget = resolveStateDir("global");
5429
+ const globalLockfile = path18.join(globalTarget, "agents-united.json");
5430
+ if (await fs16.pathExists(globalLockfile)) {
5431
+ const gLock = await fs16.readJson(globalLockfile);
4532
5432
  if (gLock.installed?.bundles?.includes(bundleName)) {
4533
5433
  return this.resolveInstallation(bundleName, { ...options, global: true });
4534
5434
  }
@@ -4536,14 +5436,14 @@ var ClaudeLauncher = class _ClaudeLauncher {
4536
5436
  }
4537
5437
  throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
4538
5438
  }
4539
- const lockfile = await fs12.readJson(lockfilePath);
5439
+ const lockfile = await fs16.readJson(lockfilePath);
4540
5440
  if (!lockfile.installed?.bundles?.includes(bundleName)) {
4541
5441
  throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
4542
5442
  }
4543
5443
  const hasClaudeFanout = (lockfile.fanout || []).includes("claude");
4544
5444
  const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
4545
- const manifestPath = path14.join(workspace, manifestRel);
4546
- if (!hasClaudeFanout && !await fs12.pathExists(manifestPath)) {
5445
+ const manifestPath = path18.join(workspace, manifestRel);
5446
+ if (!hasClaudeFanout && !await fs16.pathExists(manifestPath)) {
4547
5447
  throw new Error(
4548
5448
  `Bundle '${bundleName}' is not projected to Claude. Run 'agents update ${bundleName} --fanout claude' first.`
4549
5449
  );
@@ -4654,260 +5554,319 @@ var ClaudeLauncher = class _ClaudeLauncher {
4654
5554
  }
4655
5555
  };
4656
5556
 
4657
- // src/core/prerequisites.ts
4658
- import path16 from "path";
4659
- import os6 from "os";
4660
- import fs14 from "fs-extra";
5557
+ // src/core/antigravity-launcher.ts
5558
+ import path20 from "path";
5559
+ import os8 from "os";
5560
+ import fs18 from "fs-extra";
4661
5561
 
4662
- // src/core/mcp-locations.ts
4663
- import path15 from "path";
4664
- import os5 from "os";
4665
- import fs13 from "fs-extra";
4666
- var McpLocationRegistry = class _McpLocationRegistry {
5562
+ // src/core/antigravity-capabilities.ts
5563
+ import path19 from "path";
5564
+ import fs17 from "fs-extra";
5565
+ var AGY_MIN_VERSION = { major: 1, minor: 2, patch: 12 };
5566
+ function meetsVersionFloor(version) {
5567
+ if (!version) return false;
5568
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
5569
+ if (!match) return false;
5570
+ const [major, minor, patch] = [Number(match[1]), Number(match[2]), Number(match[3])];
5571
+ if (major !== AGY_MIN_VERSION.major) return major > AGY_MIN_VERSION.major;
5572
+ if (minor !== AGY_MIN_VERSION.minor) return minor > AGY_MIN_VERSION.minor;
5573
+ return patch >= AGY_MIN_VERSION.patch;
5574
+ }
5575
+ function parseModeValues(help) {
5576
+ const line = help.split("\n").find((candidate) => /--mode\b/.test(candidate));
5577
+ if (!line) return [];
5578
+ const afterPlaceholder = line.includes(">") ? line.slice(line.indexOf(">") + 1) : line;
5579
+ return Array.from(
5580
+ new Set(
5581
+ afterPlaceholder.split(",").map((value) => value.trim()).filter((value) => /^[a-z][a-z-]*$/.test(value))
5582
+ )
5583
+ );
5584
+ }
5585
+ var AntigravityCapabilityProbe = class {
5586
+ runner;
5587
+ customResolver;
5588
+ constructor(runner, options) {
5589
+ this.runner = runner || defaultProcessRunner;
5590
+ this.customResolver = options?.resolveExecutable;
5591
+ }
4667
5592
  /**
4668
- * Declarative catalog of known MCP configuration files across AI tools, CLIs, extensions, and workspaces.
5593
+ * Safe executable resolution across POSIX and Windows layouts.
4669
5594
  */
4670
- static LOCATIONS = [
4671
- // --- Google Antigravity & Gemini ---
4672
- {
4673
- id: "antigravity-global",
4674
- host: "gemini",
4675
- category: "cli",
4676
- label: "Google Antigravity & Gemini Global Config",
4677
- resolvePath: (_, home) => path15.join(home, ".gemini", "config", "mcp_config.json"),
4678
- isPrimary: true
4679
- },
4680
- {
4681
- id: "antigravity-system",
4682
- host: "gemini",
4683
- category: "desktop",
4684
- label: "Google Antigravity System Config",
4685
- resolvePath: (_, home) => path15.join(home, ".gemini", "antigravity", "mcp_config.json")
4686
- },
4687
- {
4688
- id: "antigravity-workspace",
4689
- host: "gemini",
4690
- category: "workspace",
4691
- label: "Google Antigravity Local Workspace Config",
4692
- resolvePath: (cwd) => path15.join(cwd, ".gemini", "config", "mcp_config.json")
4693
- },
4694
- {
4695
- id: "gemini-appdata",
4696
- host: "gemini",
4697
- category: "cli",
4698
- label: "Google Gemini AppData Config",
4699
- resolvePath: (_, __, appData) => path15.join(appData, "gemini", "config", "mcp_config.json")
4700
- },
4701
- // --- Cline CLI & VS Code Extension ---
4702
- {
4703
- id: "cline-cli-settings",
4704
- host: "cline",
4705
- category: "cli",
4706
- label: "Cline CLI MCP Settings",
4707
- resolvePath: (_, home) => path15.join(home, ".cline", "data", "settings", "cline_mcp_settings.json"),
4708
- isPrimary: true
4709
- },
4710
- {
4711
- id: "cline-extension-settings",
4712
- host: "cline",
4713
- category: "extension",
4714
- label: "Cline VS Code Extension Global Settings",
4715
- resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
4716
- },
4717
- {
4718
- id: "cline-insiders-extension-settings",
4719
- host: "cline",
4720
- category: "extension",
4721
- label: "Cline VS Code Insiders Extension Settings",
4722
- resolvePath: (_, __, appData) => path15.join(appData, "Code - Insiders", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
4723
- },
4724
- {
4725
- id: "cline-legacy-settings",
4726
- host: "cline",
4727
- category: "cli",
4728
- label: "Cline Home Legacy Settings",
4729
- resolvePath: (_, home) => path15.join(home, ".cline", "settings", "cline_mcp_settings.json")
4730
- },
4731
- {
4732
- id: "cline-home-flat",
4733
- host: "cline",
4734
- category: "cli",
4735
- label: "Cline Home Flat Settings",
4736
- resolvePath: (_, home) => path15.join(home, "cline_mcp_settings.json")
4737
- },
4738
- {
4739
- id: "cline-workspace-flat",
4740
- host: "cline",
4741
- category: "workspace",
4742
- label: "Cline Workspace Settings",
4743
- resolvePath: (cwd) => path15.join(cwd, "cline_mcp_settings.json")
4744
- },
4745
- {
4746
- id: "cline-workspace-dot",
4747
- host: "cline",
4748
- category: "workspace",
4749
- label: "Cline Workspace .cline/mcp.json",
4750
- resolvePath: (cwd) => path15.join(cwd, ".cline", "mcp.json")
4751
- },
4752
- // --- Roo Code Extension ---
4753
- {
4754
- id: "roo-code-extension-settings",
4755
- host: "cline",
4756
- category: "extension",
4757
- label: "Roo Code VS Code Extension Settings",
4758
- resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json")
4759
- },
4760
- // --- Cursor IDE / CLI ---
4761
- {
4762
- id: "cursor-workspace",
4763
- host: "cursor",
4764
- category: "workspace",
4765
- label: "Cursor Workspace Config",
4766
- resolvePath: (cwd) => path15.join(cwd, ".cursor", "mcp.json"),
4767
- isPrimary: true
4768
- },
4769
- {
4770
- id: "cursor-global",
4771
- host: "cursor",
4772
- category: "desktop",
4773
- label: "Cursor Global User Config",
4774
- resolvePath: (_, home) => path15.join(home, ".cursor", "mcp.json")
4775
- },
4776
- // --- Claude Code CLI & Claude Desktop ---
4777
- {
4778
- id: "claude-workspace-dot",
4779
- host: "claude",
4780
- category: "workspace",
4781
- label: "Claude Code Workspace Config",
4782
- resolvePath: (cwd) => path15.join(cwd, ".claude", "mcp.json"),
4783
- isPrimary: true
4784
- },
4785
- {
4786
- id: "claude-workspace-flat",
4787
- host: "claude",
4788
- category: "workspace",
4789
- label: "Claude Workspace claude.json",
4790
- resolvePath: (cwd) => path15.join(cwd, "claude.json")
4791
- },
4792
- {
4793
- id: "claude-global-dot",
4794
- host: "claude",
4795
- category: "cli",
4796
- label: "Claude Code Global Config",
4797
- resolvePath: (_, home) => path15.join(home, ".claude", "mcp.json")
4798
- },
4799
- {
4800
- id: "claude-global-flat",
4801
- host: "claude",
4802
- category: "cli",
4803
- label: "Claude Code Global claude.json",
4804
- resolvePath: (_, home) => path15.join(home, "claude.json")
4805
- },
4806
- {
4807
- id: "claude-desktop-global",
4808
- host: "claude",
4809
- category: "desktop",
4810
- label: "Claude Desktop Global Config",
4811
- resolvePath: (_, __, appData) => path15.join(appData, "Claude", "claude_desktop_config.json")
4812
- },
4813
- // --- Windsurf IDE ---
4814
- {
4815
- id: "windsurf-global",
4816
- host: "windsurf",
4817
- category: "desktop",
4818
- label: "Windsurf Global Config",
4819
- resolvePath: (_, home) => path15.join(home, ".codeium", "windsurf", "mcp_config.json"),
4820
- isPrimary: true
4821
- },
4822
- {
4823
- id: "windsurf-appdata",
4824
- host: "windsurf",
4825
- category: "desktop",
4826
- label: "Windsurf AppData Config",
4827
- resolvePath: (_, __, appData) => path15.join(appData, "Windsurf", "mcp_config.json")
4828
- },
4829
- // --- OpenCode & Codex ---
4830
- {
4831
- id: "opencode-workspace",
4832
- host: "opencode",
4833
- category: "workspace",
4834
- label: "OpenCode Workspace Config",
4835
- resolvePath: (cwd) => path15.join(cwd, ".opencode", "mcp.json"),
4836
- isPrimary: true
4837
- },
4838
- {
4839
- id: "opencode-global",
4840
- host: "opencode",
4841
- category: "cli",
4842
- label: "OpenCode Global Config",
4843
- resolvePath: (_, home) => path15.join(home, ".config", "opencode", "mcp.json")
5595
+ resolveExecutable() {
5596
+ if (this.customResolver) {
5597
+ return this.customResolver();
5598
+ }
5599
+ const envBin = process.env.AGY_BIN_PATH;
5600
+ if (envBin && path19.isAbsolute(envBin) && fs17.existsSync(envBin)) {
5601
+ return {
5602
+ executable: envBin,
5603
+ prefixArgs: [],
5604
+ source: "env-binary"
5605
+ };
5606
+ }
5607
+ const pathEnv = process.env.PATH || "";
5608
+ const pathSeparator = process.platform === "win32" ? ";" : ":";
5609
+ const dirs = pathEnv.split(pathSeparator).filter(Boolean);
5610
+ if (process.platform === "win32") {
5611
+ const exeNames = ["agy.exe", "agy.cmd", "agy.bat", "agy"];
5612
+ for (const dir of dirs) {
5613
+ for (const exeName of exeNames) {
5614
+ const fullPath = path19.join(dir, exeName);
5615
+ if (!fs17.existsSync(fullPath)) continue;
5616
+ const ext = path19.extname(fullPath).toLowerCase();
5617
+ if (ext === ".cmd" || ext === ".bat") {
5618
+ return {
5619
+ executable: "cmd.exe",
5620
+ prefixArgs: ["/c", fullPath],
5621
+ source: "path-executable"
5622
+ };
5623
+ }
5624
+ return {
5625
+ executable: fullPath,
5626
+ prefixArgs: [],
5627
+ source: "path-executable"
5628
+ };
5629
+ }
5630
+ }
5631
+ } else {
5632
+ for (const dir of dirs) {
5633
+ const fullPath = path19.join(dir, "agy");
5634
+ if (fs17.existsSync(fullPath)) {
5635
+ return {
5636
+ executable: fullPath,
5637
+ prefixArgs: [],
5638
+ source: "path-executable"
5639
+ };
5640
+ }
5641
+ }
4844
5642
  }
4845
- ];
5643
+ return null;
5644
+ }
4846
5645
  /**
4847
- * Discovers all MCP configuration files across the filesystem, inspecting existence and parsing server definitions.
5646
+ * Run the read-only capability probe against the local Antigravity CLI. Never throws: an absent
5647
+ * executable, a non-zero exit and a runner error are all reported as data.
4848
5648
  */
4849
- static async discoverAll(cwd = process.cwd()) {
4850
- const homeDir = os5.homedir();
4851
- const appData = process.env.APPDATA || (process.platform === "darwin" ? path15.join(homeDir, "Library", "Application Support") : path15.join(homeDir, ".config"));
4852
- const results = [];
4853
- const seenPaths = /* @__PURE__ */ new Set();
4854
- for (const desc of _McpLocationRegistry.LOCATIONS) {
4855
- const resolved = path15.normalize(desc.resolvePath(cwd, homeDir, appData));
4856
- const key = resolved.toLowerCase();
4857
- if (seenPaths.has(key)) continue;
4858
- seenPaths.add(key);
4859
- const exists = await fs13.pathExists(resolved);
4860
- let serverCount = 0;
4861
- let servers;
4862
- if (exists) {
4863
- try {
4864
- const config = await fs13.readJson(resolved);
4865
- const rawServers = config?.mcpServers || config?.servers || config;
4866
- if (rawServers && typeof rawServers === "object") {
4867
- servers = rawServers;
4868
- serverCount = Object.keys(rawServers).length;
4869
- }
4870
- } catch {
4871
- }
5649
+ async probe() {
5650
+ const cmd = this.resolveExecutable();
5651
+ const diagnostics = [];
5652
+ if (!cmd) {
5653
+ diagnostics.push("Antigravity CLI (agy) not found on PATH or via AGY_BIN_PATH.");
5654
+ return {
5655
+ installed: false,
5656
+ agentFlag: false,
5657
+ promptInteractive: false,
5658
+ modes: [],
5659
+ meetsVersionFloor: false,
5660
+ diagnostics
5661
+ };
5662
+ }
5663
+ let version;
5664
+ try {
5665
+ const verRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "--version"], {
5666
+ timeoutMs: 5e3
5667
+ });
5668
+ if (verRes.exitCode === 0) {
5669
+ version = verRes.stdout.trim().split("\n")[0]?.trim();
5670
+ } else {
5671
+ diagnostics.push(
5672
+ `Antigravity CLI version check failed (exit code ${verRes.exitCode}): ${verRes.stderr.trim()}`
5673
+ );
5674
+ return {
5675
+ installed: false,
5676
+ command: cmd,
5677
+ agentFlag: false,
5678
+ promptInteractive: false,
5679
+ modes: [],
5680
+ meetsVersionFloor: false,
5681
+ diagnostics
5682
+ };
4872
5683
  }
4873
- results.push({
4874
- id: desc.id,
4875
- host: desc.host,
4876
- category: desc.category,
4877
- label: desc.label,
4878
- path: resolved,
4879
- exists,
4880
- serverCount,
4881
- servers,
4882
- isPrimary: desc.isPrimary
5684
+ } catch (err) {
5685
+ const msg = err instanceof Error ? err.message : String(err);
5686
+ diagnostics.push(`Error executing Antigravity CLI version check: ${msg}`);
5687
+ return {
5688
+ installed: false,
5689
+ command: cmd,
5690
+ agentFlag: false,
5691
+ promptInteractive: false,
5692
+ modes: [],
5693
+ meetsVersionFloor: false,
5694
+ diagnostics
5695
+ };
5696
+ }
5697
+ let agentFlag = false;
5698
+ let promptInteractive = false;
5699
+ let modes = [];
5700
+ try {
5701
+ const helpRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "--help"], {
5702
+ timeoutMs: 5e3
4883
5703
  });
5704
+ if (helpRes.exitCode === 0) {
5705
+ const help = helpRes.stdout;
5706
+ agentFlag = help.includes("--agent");
5707
+ promptInteractive = help.includes("--prompt-interactive");
5708
+ modes = parseModeValues(help);
5709
+ } else {
5710
+ diagnostics.push(
5711
+ `Antigravity CLI help probe returned non-zero (exit code ${helpRes.exitCode}); the --agent / --prompt-interactive / --mode surface could not be verified.`
5712
+ );
5713
+ }
5714
+ } catch (err) {
5715
+ const msg = err instanceof Error ? err.message : String(err);
5716
+ diagnostics.push(`Antigravity CLI help probe failed: ${msg}`);
4884
5717
  }
4885
- return results;
5718
+ return {
5719
+ installed: true,
5720
+ version,
5721
+ command: cmd,
5722
+ agentFlag,
5723
+ promptInteractive,
5724
+ modes,
5725
+ meetsVersionFloor: meetsVersionFloor(version),
5726
+ diagnostics
5727
+ };
5728
+ }
5729
+ };
5730
+
5731
+ // src/core/antigravity-launcher.ts
5732
+ var DEFAULT_COORDINATOR2 = "orchestrator-engineering.md";
5733
+ var AntigravityLauncher = class _AntigravityLauncher {
5734
+ probe;
5735
+ runner;
5736
+ constructor(probe, runner) {
5737
+ this.probe = probe || new AntigravityCapabilityProbe();
5738
+ this.runner = runner || defaultProcessRunner;
4886
5739
  }
4887
5740
  /**
4888
- * Returns discovered locations filtered for specific host(s).
5741
+ * The `--agent` value must equal the canonical coordinator definition's basename without `.md`.
5742
+ *
5743
+ * Mirrors `ClaudeLauncher.resolveCoordinatorName` (same strip of a leading `subagent-` prefix and
5744
+ * the `.md` suffix) without importing the Claude lane: on Antigravity the canonical file under
5745
+ * `.agents/agents/` is installed verbatim, so its basename *is* the agent name.
4889
5746
  */
4890
- static async discoverForHosts(hosts, cwd = process.cwd()) {
4891
- const all = await _McpLocationRegistry.discoverAll(cwd);
4892
- const hostSet = new Set(hosts.map((h) => h === "agents" ? "gemini" : h));
4893
- return all.filter((loc) => hostSet.has(loc.host));
5747
+ static resolveCoordinatorName(orchestrator) {
5748
+ const file = (orchestrator && orchestrator.trim().length > 0 ? orchestrator.trim() : DEFAULT_COORDINATOR2).replace(/\.md$/i, "");
5749
+ return path20.basename(file).replace(/^subagent-/, "");
4894
5750
  }
4895
5751
  /**
4896
- * Resolves the primary target write path for a given host runtime.
5752
+ * Resolve an installed bundle for Antigravity activation. Mirrors
5753
+ * `ClaudeLauncher.resolveInstallation`: same state-dir discovery (`.agents/` store or the
5754
+ * store-less sidecar), same global→project fallback, same error text.
5755
+ *
5756
+ * No host gate is applied here on purpose. The `agents` host IS the canonical store, so a bundle
5757
+ * present in this lockfile is by definition projected to the Antigravity lane; requiring a
5758
+ * separate fanout signal would make `agents start --host antigravity` fail on exactly the install
5759
+ * it is meant to launch. Whether the canonical coordinator file exists is checked by the caller
5760
+ * before anything is spawned.
4897
5761
  */
4898
- static getPrimaryWritePath(host, cwd = process.cwd()) {
4899
- const homeDir = os5.homedir();
4900
- const appData = process.env.APPDATA || (process.platform === "darwin" ? path15.join(homeDir, "Library", "Application Support") : path15.join(homeDir, ".config"));
4901
- const targetHost = host === "agents" ? "gemini" : host;
4902
- const desc = _McpLocationRegistry.LOCATIONS.find((l) => l.host === targetHost && l.isPrimary);
4903
- if (desc) {
4904
- return path15.normalize(desc.resolvePath(cwd, homeDir, appData));
5762
+ async resolveInstallation(bundleName, options = {}) {
5763
+ const cwd = options.cwd || process.cwd();
5764
+ const isGlobal = !!options.global;
5765
+ let targetDir;
5766
+ let scope;
5767
+ let workspace;
5768
+ if (isGlobal) {
5769
+ targetDir = resolveStateDir("global");
5770
+ scope = "global";
5771
+ workspace = os8.homedir();
5772
+ } else {
5773
+ workspace = cwd;
5774
+ targetDir = resolveStateDir("project", void 0, { cwd });
5775
+ scope = "project";
4905
5776
  }
4906
- return path15.normalize(path15.join(cwd, ".cursor", "mcp.json"));
5777
+ const lockfilePath = path20.join(targetDir, "agents-united.json");
5778
+ if (!await fs18.pathExists(lockfilePath)) {
5779
+ if (!isGlobal) {
5780
+ const globalTarget = resolveStateDir("global");
5781
+ const globalLockfile = path20.join(globalTarget, "agents-united.json");
5782
+ if (await fs18.pathExists(globalLockfile)) {
5783
+ const gLock = await fs18.readJson(globalLockfile);
5784
+ if (gLock.installed?.bundles?.includes(bundleName)) {
5785
+ return this.resolveInstallation(bundleName, { ...options, global: true });
5786
+ }
5787
+ }
5788
+ }
5789
+ throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
5790
+ }
5791
+ const lockfile = await fs18.readJson(lockfilePath);
5792
+ if (!lockfile.installed?.bundles?.includes(bundleName)) {
5793
+ throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
5794
+ }
5795
+ const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
5796
+ return {
5797
+ scope,
5798
+ workspace,
5799
+ lockfile,
5800
+ manifestPath: path20.join(workspace, manifestRel)
5801
+ };
5802
+ }
5803
+ /**
5804
+ * Plan the argv and bootstrap prompt for an Antigravity session. Pure: no process is spawned and
5805
+ * nothing is written to disk, so `--dry-run` can call it safely.
5806
+ */
5807
+ planActivation(options) {
5808
+ const { bundleName, workspace, scope, report, prompt, orchestrator } = options;
5809
+ const command = report.command || {
5810
+ executable: "agy",
5811
+ prefixArgs: [],
5812
+ source: "path-executable"
5813
+ };
5814
+ const coordinatorName = _AntigravityLauncher.resolveCoordinatorName(orchestrator);
5815
+ const coordinatorCanonical = `.agents/agents/${coordinatorName}.md`;
5816
+ const manifestRel = scope === "global" ? `~/.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml` : `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
5817
+ const taskText = prompt && prompt.trim().length > 0 ? `User task: ${prompt.trim()}` : "Please introduce your coordinator role to the user and ask for their first task.";
5818
+ const bootstrapPrompt = [
5819
+ `You are coordinating the "${bundleName}" team in Agents United.`,
5820
+ `Read the Team Manifest at "${manifestRel}" and your coordinator role definition at "${coordinatorCanonical}" before acting.`,
5821
+ `Delegate specialist work with the invoke_subagent tool; register a specialist with define_subagent only when it is not already reachable by name, and pass the canonical role body verbatim rather than a summary.`,
5822
+ `Use specialist roles only when necessary.`,
5823
+ `Before installing any recommended addon, explain the requirement to the user and request explicit confirmation to run: agents add <addon> -t agents -y.`,
5824
+ taskText
5825
+ ].join("\n\n");
5826
+ const env = {};
5827
+ const argv = [...command.prefixArgs];
5828
+ argv.push("--agent");
5829
+ argv.push(coordinatorName);
5830
+ if (prompt && prompt.trim().length > 0) {
5831
+ argv.push("--prompt-interactive");
5832
+ argv.push(prompt);
5833
+ }
5834
+ return {
5835
+ bundleName,
5836
+ scope,
5837
+ workspace,
5838
+ executable: command.executable,
5839
+ argv,
5840
+ bootstrapPrompt,
5841
+ env
5842
+ };
5843
+ }
5844
+ /**
5845
+ * Launch the Antigravity session from the argv array. Mirrors `ClaudeLauncher.launch`: no shell is
5846
+ * ever used to interpret the command (`shell: false` on the spawn options below), `stdio: 'inherit'`,
5847
+ * and the plan's ephemeral env merged over `process.env`.
5848
+ */
5849
+ async launch(plan) {
5850
+ const { spawn } = await import("child_process");
5851
+ const child = spawn(plan.executable, plan.argv, {
5852
+ cwd: plan.workspace,
5853
+ stdio: "inherit",
5854
+ shell: false,
5855
+ env: { ...process.env, ...plan.env }
5856
+ });
5857
+ await new Promise((resolve, reject) => {
5858
+ child.on("error", reject);
5859
+ child.on("exit", () => {
5860
+ resolve();
5861
+ });
5862
+ });
4907
5863
  }
4908
5864
  };
4909
5865
 
4910
5866
  // src/core/prerequisites.ts
5867
+ import path21 from "path";
5868
+ import os9 from "os";
5869
+ import fs19 from "fs-extra";
4911
5870
  var PrerequisiteChecker = class _PrerequisiteChecker {
4912
5871
  /**
4913
5872
  * Discovers all candidate and existing MCP configuration files across AI hosts
@@ -5018,21 +5977,21 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
5018
5977
  });
5019
5978
  }
5020
5979
  }
5021
- const homeDir = os6.homedir();
5980
+ const homeDir = os9.homedir();
5022
5981
  const dirCandidates = [
5023
- path16.join(cwd, ".gemini", "antigravity", "mcp"),
5024
- path16.join(homeDir, ".gemini", "antigravity", "mcp")
5982
+ path21.join(cwd, ".gemini", "antigravity", "mcp"),
5983
+ path21.join(homeDir, ".gemini", "antigravity", "mcp")
5025
5984
  ];
5026
5985
  for (const dir of dirCandidates) {
5027
- if (await fs14.pathExists(dir)) {
5028
- const entries = await fs14.readdir(dir).catch(() => []);
5986
+ if (await fs19.pathExists(dir)) {
5987
+ const entries = await fs19.readdir(dir).catch(() => []);
5029
5988
  for (const entry of entries) {
5030
5989
  if (_PrerequisiteChecker.matchesMcpServer(mcpName, entry, {})) {
5031
5990
  foundInConfigs.push({
5032
5991
  id: "antigravity-dir",
5033
5992
  host: "gemini",
5034
5993
  label: "Google Antigravity MCP Directory",
5035
- path: path16.join(dir, entry),
5994
+ path: path21.join(dir, entry),
5036
5995
  disabled: false,
5037
5996
  serverKey: entry
5038
5997
  });
@@ -5057,14 +6016,14 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
5057
6016
  * Checks if an npm package is installed in node_modules or declared in package.json
5058
6017
  */
5059
6018
  static async isPackageInstalled(pkgName, cwd = process.cwd()) {
5060
- const modulePath = path16.join(cwd, "node_modules", ...pkgName.split("/"));
5061
- if (await fs14.pathExists(modulePath)) {
6019
+ const modulePath = path21.join(cwd, "node_modules", ...pkgName.split("/"));
6020
+ if (await fs19.pathExists(modulePath)) {
5062
6021
  return { installed: true, location: modulePath };
5063
6022
  }
5064
- const pkgJsonPath = path16.join(cwd, "package.json");
5065
- if (await fs14.pathExists(pkgJsonPath)) {
6023
+ const pkgJsonPath = path21.join(cwd, "package.json");
6024
+ if (await fs19.pathExists(pkgJsonPath)) {
5066
6025
  try {
5067
- const pkg = await fs14.readJson(pkgJsonPath);
6026
+ const pkg = await fs19.readJson(pkgJsonPath);
5068
6027
  const deps = {
5069
6028
  ...pkg.dependencies,
5070
6029
  ...pkg.devDependencies,
@@ -5087,20 +6046,20 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
5087
6046
  return { set: true, source: "process.env" };
5088
6047
  }
5089
6048
  const envFiles = [
5090
- path16.join(cwd, ".env.local"),
5091
- path16.join(cwd, ".env")
6049
+ path21.join(cwd, ".env.local"),
6050
+ path21.join(cwd, ".env")
5092
6051
  ];
5093
6052
  for (const envFile of envFiles) {
5094
- if (await fs14.pathExists(envFile)) {
6053
+ if (await fs19.pathExists(envFile)) {
5095
6054
  try {
5096
- const content = await fs14.readFile(envFile, "utf8");
6055
+ const content = await fs19.readFile(envFile, "utf8");
5097
6056
  const lines = content.split("\n");
5098
6057
  for (const line of lines) {
5099
6058
  const trimmed = line.trim();
5100
6059
  if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
5101
6060
  const [k, ...v] = trimmed.split("=");
5102
6061
  if (k.trim() === varName && v.join("=").trim() !== "") {
5103
- return { set: true, source: path16.basename(envFile) };
6062
+ return { set: true, source: path21.basename(envFile) };
5104
6063
  }
5105
6064
  }
5106
6065
  } catch {
@@ -5306,11 +6265,11 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
5306
6265
  * activating disabled servers and appending missing definitions.
5307
6266
  */
5308
6267
  static async autoConfigureConfigFile(targetFilePath, mcpNames, mode = "operational", envVars = {}) {
5309
- await fs14.ensureDir(path16.dirname(targetFilePath));
6268
+ await fs19.ensureDir(path21.dirname(targetFilePath));
5310
6269
  let existingConfig = {};
5311
- if (await fs14.pathExists(targetFilePath)) {
6270
+ if (await fs19.pathExists(targetFilePath)) {
5312
6271
  try {
5313
- existingConfig = await fs14.readJson(targetFilePath);
6272
+ existingConfig = await fs19.readJson(targetFilePath);
5314
6273
  } catch {
5315
6274
  existingConfig = {};
5316
6275
  }
@@ -5342,7 +6301,7 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
5342
6301
  } else {
5343
6302
  existingConfig.mcpServers = existingServers;
5344
6303
  }
5345
- await fs14.writeJson(targetFilePath, existingConfig, { spaces: 2 });
6304
+ await fs19.writeJson(targetFilePath, existingConfig, { spaces: 2 });
5346
6305
  return {
5347
6306
  success: true,
5348
6307
  targetFile: targetFilePath,
@@ -5387,7 +6346,7 @@ var updater = new UpdateEngine(registry, scanner, installer);
5387
6346
  function detectWorkspaceHosts(cwd = process.cwd()) {
5388
6347
  const detected = [];
5389
6348
  for (const host of Object.values(HOST_REGISTRY)) {
5390
- const found = host.detectionMarkers.some((marker) => fs15.pathExistsSync(path17.join(cwd, marker)));
6349
+ const found = host.detectionMarkers.some((marker) => fs20.pathExistsSync(path22.join(cwd, marker)));
5391
6350
  if (found) detected.push(host.id);
5392
6351
  }
5393
6352
  return detected;
@@ -5618,12 +6577,12 @@ var BUNDLE_DISPLAY_NAMES = {
5618
6577
  summary: "Complete suite with all 7 team leads, 38 agents, and all 160 modular skills & workflows"
5619
6578
  }
5620
6579
  };
5621
- cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to project or global configuration").option("-g, --global", "Install globally into home directory (~/.agents/)").option("-s, --symlink", "Create symbolic links to central registry cache (default / recommended)").option("--copy", "Create independent standalone copies of asset files").option("-t, --target <hosts>", "Which assistants to set up (agents = main library; claude, cursor, cline, opencode, codex get translated copies)", { default: "agents" }).option("--fanout <hosts>", "Also make translated copies for these assistants: claude, cline. Under Development hosts (cursor, opencode, codex) are refused").option("--plugin", "Claude lane only: also emit the distribution-only plugin package (.agents/plugins/<bundle>/.claude-plugin/plugin.json + agents/) for `claude --plugin-dir`. Adds nothing when --fanout claude is absent; never the behavioural source. Sticky: the opt-in is recorded in the lockfile, so `agents update` keeps it. Use --no-plugin to turn it back off.").option("--mode <mode>", "Execution mode for organization bundles (operational | brainstorming)", { default: "operational" }).option("--allow-missing-prereqs", "Proceed with installation even if some prerequisites are missing").option("--allow-under-construction", "Allow installation of bundles marked as under construction").option("-y, --yes", "Skip confirmation prompts").option("-f, --force", "Force overwrite user modified files").option("--start", "Start the installed team in Cline immediately after setup").option("--dry-run", "Simulate installation without writing files").action(async (targetIdentifier, options = {}) => {
6580
+ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to project or global configuration").option("-g, --global", "Install globally into home directory (~/.agents/)").option("-s, --symlink", "Create symbolic links to central registry cache (default / recommended)").option("--copy", "Create independent standalone copies of asset files").option("-t, --target <hosts>", "Which assistants to set up (agents = main library; claude, cursor, cline, opencode, codex get translated copies)", { default: "agents" }).option("--fanout <hosts>", "Also make translated copies for these assistants: claude, cline. Under Development hosts (cursor, opencode, codex) are refused").option("--plugin", "Claude lane only: also emit the distribution-only plugin package (.agents/plugins/<bundle>/.claude-plugin/plugin.json + agents/) for `claude --plugin-dir`. Adds nothing when --fanout claude is absent; never the behavioural source. Sticky: the opt-in is recorded in the lockfile, so `agents update` keeps it. Use --no-plugin to turn it back off.").option("--canonical-store", "Keep the .agents/ main library even for a Claude-only install (by default a Claude-only install is store-less: its state lives in the hidden .claude/.agents-united/ folder, ADR 0022)").option("--session-guard [where]", "Claude lane only: also guard PLAIN Claude sessions (no --agent) by adding one managed hook entry that blocks `git push --force`, `.env` writes and `vercel --prod`. where = project (.claude/settings.json, default) | local (.claude/settings.local.json) | user (~/.claude/settings.json). Everything else in the file is kept; invalid JSON is never rewritten. Sticky; --no-session-guard turns it off.").option("--permission-preset [tier]", 'Opt-in only, never implied by -y: pre-approve a small, fixed set of commands in .claude/settings.local.json (never the shared/committed settings.json) so background specialists are not refused command approval outside auto mode. tier = verify (default: git status/diff/log, npx tsc/vitest/eslint) | build (adds npm install/run/test \u2014 executes project code, request explicitly). Never includes git commit/push, rm, curl/wget, or any deploy/publish command. Workspace-scoped only (ignored for --global). Sticky; --no-permission-preset turns it off. Claude only today; other hosts report "not supported yet".').option("--mode <mode>", "Execution mode for organization bundles (operational | brainstorming)", { default: "operational" }).option("--allow-missing-prereqs", "Proceed with installation even if some prerequisites are missing").option("--allow-under-construction", "Allow installation of bundles marked as under construction").option("-y, --yes", "Skip confirmation prompts").option("-f, --force", "Force overwrite user modified files").option("--start", "Start the installed team in Cline immediately after setup").option("--dry-run", "Simulate installation without writing files").action(async (targetIdentifier, options = {}) => {
5622
6581
  intro(pc.cyan("Agents United \u2014 AI Agent Ecosystem"));
5623
6582
  let identifier = targetIdentifier;
5624
6583
  let scope = options.global ? "global" : "project";
5625
6584
  let method = options.copy ? "copy" : "symlink";
5626
- let hosts = options.target ? (Array.isArray(options.target) ? options.target : options.target.split(",")).map(
6585
+ let hosts = options.target ? (Array.isArray(options.target) ? options.target : splitHostList(String(options.target))).map(
5627
6586
  (h) => h.trim().toLowerCase()
5628
6587
  ) : ["agents"];
5629
6588
  const unknownHosts = hosts.filter((h) => !isKnownHost(h));
@@ -5632,6 +6591,7 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
5632
6591
  hosts = hosts.filter(isKnownHost);
5633
6592
  if (hosts.length === 0) hosts = ["agents"];
5634
6593
  }
6594
+ let selectedHosts = [...hosts];
5635
6595
  const flagPlan = planInstallTargets(hosts);
5636
6596
  hosts = flagPlan.hosts;
5637
6597
  if (flagPlan.addedCanonicalStore) {
@@ -5642,7 +6602,7 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
5642
6602
  }
5643
6603
  let fanout = flagPlan.fanout;
5644
6604
  if (options.fanout) {
5645
- const rawFanout = Array.isArray(options.fanout) ? options.fanout : String(options.fanout).split(",");
6605
+ const rawFanout = Array.isArray(options.fanout) ? options.fanout : splitHostList(String(options.fanout));
5646
6606
  const parsedFanout = rawFanout.map((h) => h.trim().toLowerCase());
5647
6607
  const invalidFanout = parsedFanout.filter((h) => !isKnownHost(h) || !HOST_REGISTRY[h].projectionCapable);
5648
6608
  if (invalidFanout.length > 0) {
@@ -5700,6 +6660,7 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
5700
6660
  });
5701
6661
  if (Array.isArray(hostSelection) && hostSelection.length > 0) {
5702
6662
  const wizardPlan = planInstallTargets(hostSelection);
6663
+ selectedHosts = [...hostSelection];
5703
6664
  hosts = wizardPlan.hosts;
5704
6665
  fanout = Array.from(/* @__PURE__ */ new Set([...fanout, ...wizardPlan.fanout]));
5705
6666
  if (wizardPlan.addedCanonicalStore) {
@@ -5746,6 +6707,42 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
5746
6707
  method = methodSelection;
5747
6708
  }
5748
6709
  }
6710
+ let sessionGuard;
6711
+ if (options.sessionGuard === false) {
6712
+ sessionGuard = false;
6713
+ } else if (options.sessionGuard === true || options.sessionGuard === "") {
6714
+ sessionGuard = "project";
6715
+ } else if (typeof options.sessionGuard === "string") {
6716
+ const where = options.sessionGuard.trim().toLowerCase();
6717
+ if (where !== "project" && where !== "local" && where !== "user") {
6718
+ note(`--session-guard accepts project, local or user (got "${options.sessionGuard}").`, "Invalid option");
6719
+ process.exit(1);
6720
+ }
6721
+ sessionGuard = where;
6722
+ } else if (isInteractive && fanout.includes("claude")) {
6723
+ const recordedLockfile = path22.join(resolveStateDir(scope), "agents-united.json");
6724
+ const recorded = await fs20.readJson(recordedLockfile).catch(() => null);
6725
+ if (!recorded?.sessionGuard) {
6726
+ const answer = await confirm({
6727
+ message: scope === "global" ? "Also guard plain Claude sessions on this machine? (blocks git push --force, .env writes and vercel --prod in ~/.claude/settings.json)" : "Also guard plain Claude sessions in this repo? (blocks git push --force, .env writes and vercel --prod via .claude/settings.json)",
6728
+ initialValue: true
6729
+ });
6730
+ if (typeof answer === "boolean") sessionGuard = answer ? scope === "global" ? "user" : "project" : false;
6731
+ }
6732
+ }
6733
+ let permissionPreset;
6734
+ if (options.permissionPreset === false) {
6735
+ permissionPreset = false;
6736
+ } else if (options.permissionPreset === true || options.permissionPreset === "") {
6737
+ permissionPreset = "verify";
6738
+ } else if (typeof options.permissionPreset === "string") {
6739
+ const tier = options.permissionPreset.trim().toLowerCase();
6740
+ if (tier !== "verify" && tier !== "build") {
6741
+ note(`--permission-preset accepts verify or build (got "${options.permissionPreset}").`, "Invalid option");
6742
+ process.exit(1);
6743
+ }
6744
+ permissionPreset = tier;
6745
+ }
5749
6746
  if (!identifier) {
5750
6747
  const bundles = await registry.listBundles();
5751
6748
  const domainMeta = {
@@ -5992,6 +6989,16 @@ To bypass this gate and install draft assets anyway, pass: --allow-under-constru
5992
6989
  }
5993
6990
  }
5994
6991
  }
6992
+ const storeShape = planInstallTargets([...selectedHosts, ...fanout], {
6993
+ canonicalStore: options.canonicalStore === true,
6994
+ pluginLane: options.plugin === true
6995
+ }).storeShape;
6996
+ if (storeShape === "sidecar" && !options.dryRun) {
6997
+ note(
6998
+ "Claude only: no .agents/ main library is created. The install state is kept in the hidden\n.claude/.agents-united/ folder (machine-owned \u2014 do not edit). Add --canonical-store to keep .agents/.",
6999
+ "Claude-only install"
7000
+ );
7001
+ }
5995
7002
  const s = spinner();
5996
7003
  s.start(`Resolving "${identifier}"...`);
5997
7004
  try {
@@ -6004,6 +7011,9 @@ To bypass this gate and install draft assets anyway, pass: --allow-under-constru
6004
7011
  // claude lane runs). `--plugin` opts in, `--no-plugin` opts out explicitly, and an omitted
6005
7012
  // flag inherits the recorded choice so `agents update` cannot prune the package.
6006
7013
  pluginLane: typeof options.plugin === "boolean" ? options.plugin : void 0,
7014
+ sessionGuard,
7015
+ permissionPreset,
7016
+ storeShape,
6007
7017
  mode: executionMode,
6008
7018
  allowMissingPrereqs: options.allowMissingPrereqs,
6009
7019
  yes: options.yes,
@@ -6231,7 +7241,7 @@ cli.command("update [identifier]", "Update installed packages to upstream regist
6231
7241
  intro(pc.cyan("Agents United \u2014 Package Update Engine"));
6232
7242
  let updateFanout;
6233
7243
  if (options.fanout !== void 0) {
6234
- const rawFanout = Array.isArray(options.fanout) ? options.fanout : String(options.fanout).split(",");
7244
+ const rawFanout = Array.isArray(options.fanout) ? options.fanout : splitHostList(String(options.fanout));
6235
7245
  const parsedFanout = rawFanout.map((h) => h.trim().toLowerCase()).filter(Boolean);
6236
7246
  const invalidFanout = parsedFanout.filter((h) => !isKnownHost(h) || !HOST_REGISTRY[h].projectionCapable);
6237
7247
  if (invalidFanout.length > 0) {
@@ -6374,7 +7384,7 @@ Updates Available: ${report.outdatedCount > 0 ? pc.yellow(pc.bold(`${report.outd
6374
7384
  const unprojected = [];
6375
7385
  for (const rec of result.updated) {
6376
7386
  try {
6377
- const lf = await fs15.readJson(path17.join(rec.targetDir, "agents-united.json"));
7387
+ const lf = await fs20.readJson(path22.join(rec.targetDir, "agents-united.json"));
6378
7388
  if (!lf.fanout || lf.fanout.length === 0) unprojected.push(rec.name);
6379
7389
  } catch {
6380
7390
  }
@@ -7097,7 +8107,7 @@ cli.command("init", "Initialize project workspace directory and install recommen
7097
8107
  const s = spinner();
7098
8108
  s.start(`Initializing workspace with bundle "${options.bundle}"...`);
7099
8109
  try {
7100
- const initTargets = (Array.isArray(options.target) ? options.target : String(options.target || "agents").split(",")).map((h) => h.trim().toLowerCase());
8110
+ const initTargets = (Array.isArray(options.target) ? options.target : splitHostList(String(options.target || "agents"))).map((h) => h.trim().toLowerCase());
7101
8111
  const initPlan = planInstallTargets(initTargets);
7102
8112
  const result = await installer.install(options.bundle, {
7103
8113
  scope: "project",
@@ -7124,8 +8134,20 @@ cli.command("start <bundle> [prompt]", "Start an installed bundle team in its ho
7124
8134
  await runClaudeStart(bundle, prompt, options);
7125
8135
  return;
7126
8136
  }
8137
+ if (effectiveHost === "antigravity") {
8138
+ await runAntigravityStart(bundle, prompt, options);
8139
+ return;
8140
+ }
7127
8141
  if (effectiveHost === "unsupported") {
7128
- note(pc.yellow(`Host '${options.host}' has no activation launcher yet; falling back to the Cline lane.`), "Host Runtime");
8142
+ note(
8143
+ pc.yellow(
8144
+ `Host '${options.host}' has no activation launcher. Launcher-capable lanes: claude, antigravity, cline.
8145
+ Re-run with --host claude|antigravity|cline to choose one explicitly.`
8146
+ ),
8147
+ "Host Runtime"
8148
+ );
8149
+ outro(pc.yellow("No session launched."));
8150
+ return;
7129
8151
  }
7130
8152
  const resolution = await launcher.resolveInstallation(bundle, {
7131
8153
  global: options.global,
@@ -7222,6 +8244,7 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
7222
8244
  report.warnings.forEach((w) => console.log(` \u26A0 ${w}`));
7223
8245
  console.log();
7224
8246
  }
8247
+ printDeclaredDeltas(report, options.host);
7225
8248
  outro(pc.blue("\u2728 Workspace is ready for initialization."));
7226
8249
  return;
7227
8250
  }
@@ -7229,6 +8252,27 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
7229
8252
  console.log(` \u26A1 Installed Skills: ${pc.bold(report.skillsCount.toString())}`);
7230
8253
  console.log(` \u{1F504} Installed Workflow Skills: ${pc.bold(report.workflowsCount.toString())}
7231
8254
  `);
8255
+ if (report.sessionGuard) {
8256
+ const label = {
8257
+ wired: pc.green("\u2714 Wired (plain Claude sessions guarded)"),
8258
+ off: pc.dim("Off (declined)"),
8259
+ missing: pc.yellow("\u2716 Missing \u2014 run agents update"),
8260
+ modified: pc.yellow("\u26A0 Edited by hand \u2014 left as-is"),
8261
+ "skipped-invalid": pc.yellow("\u26A0 Not wired \u2014 settings file is not valid JSON (see warning)")
8262
+ };
8263
+ console.log(` \u{1F6E1} Session Guard: ${label[report.sessionGuard]}
8264
+ `);
8265
+ }
8266
+ if (report.permissionPreset) {
8267
+ const label = {
8268
+ wired: pc.green("\u2714 Wired (npm test/tsc/vitest/eslint pre-approved)"),
8269
+ off: pc.dim("Off (declined)"),
8270
+ missing: pc.yellow("\u2716 Missing or partially removed \u2014 run agents update"),
8271
+ "skipped-invalid": pc.yellow("\u26A0 Not wired \u2014 settings.local.json is not valid JSON (see warning)")
8272
+ };
8273
+ console.log(` \u{1F511} Permission Preset: ${label[report.permissionPreset]}
8274
+ `);
8275
+ }
7232
8276
  if (report.clineCapability) {
7233
8277
  console.log(pc.bold(pc.cyan("Cline Runtime & Native Discovery Audit:")));
7234
8278
  console.log(` Installed: ${report.clineCapability.installed ? pc.green("\u2714 Detected") : pc.yellow("\u2716 Not Found")}`);
@@ -7254,6 +8298,7 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
7254
8298
  report.warnings.forEach((w) => console.log(` \u26A0 ${w}`));
7255
8299
  console.log();
7256
8300
  }
8301
+ printDeclaredDeltas(report, options.host);
7257
8302
  if (report.valid && report.issues.length === 0) {
7258
8303
  outro(pc.green("\u2714 All installed agents and frontmatter schemas are healthy!"));
7259
8304
  } else {
@@ -7270,19 +8315,108 @@ async function resolveStartHost(bundle, options) {
7270
8315
  return "cline";
7271
8316
  }
7272
8317
  if (explicitHost.length > 0) {
8318
+ if (explicitHost === "antigravity") {
8319
+ return "antigravity";
8320
+ }
8321
+ if (isKnownHost(explicitHost) && HOST_REGISTRY[explicitHost].profile === "antigravity") {
8322
+ return "antigravity";
8323
+ }
7273
8324
  return "unsupported";
7274
8325
  }
8326
+ let recordedHosts = [];
7275
8327
  let recordedFanout = [];
7276
8328
  try {
7277
- const detection = await new ClaudeLauncher().resolveInstallation(bundle, {
8329
+ const detection = await new AntigravityLauncher().resolveInstallation(bundle, {
7278
8330
  global: options.global,
7279
8331
  cwd: process.cwd()
7280
8332
  });
8333
+ recordedHosts = detection.lockfile.hosts || [];
7281
8334
  recordedFanout = detection.lockfile.fanout || [];
7282
8335
  } catch {
8336
+ recordedHosts = [];
7283
8337
  recordedFanout = [];
7284
8338
  }
7285
- return recordedFanout.includes("claude") ? "claude" : "cline";
8339
+ if (recordedFanout.includes("claude")) return "claude";
8340
+ if (recordedHosts.includes("cline") || recordedFanout.includes("cline")) return "cline";
8341
+ const isAntigravityProfile = (host) => isKnownHost(host) && HOST_REGISTRY[host].profile === "antigravity";
8342
+ if (recordedHosts.length > 0 && recordedHosts.every(isAntigravityProfile)) return "antigravity";
8343
+ return "cline";
8344
+ }
8345
+ async function runAntigravityStart(bundle, prompt, options) {
8346
+ const launcher = new AntigravityLauncher();
8347
+ const probe = new AntigravityCapabilityProbe();
8348
+ const resolution = await launcher.resolveInstallation(bundle, {
8349
+ global: options.global,
8350
+ cwd: process.cwd()
8351
+ });
8352
+ const bundleDef = await registry.getBundle(bundle);
8353
+ const coordinatorName = AntigravityLauncher.resolveCoordinatorName(bundleDef?.orchestrator);
8354
+ const coordinatorRel = path22.posix.join(".agents", "agents", `${coordinatorName}.md`);
8355
+ const probeReport = await probe.probe();
8356
+ if (!probeReport.installed) {
8357
+ note(
8358
+ pc.yellow(
8359
+ `Host: ${pc.cyan("antigravity")} \u2014 the agy CLI was not found on PATH or via AGY_BIN_PATH.
8360
+
8361
+ Desktop route (nothing was launched):
8362
+ 1. Open this workspace in the Antigravity app.
8363
+ 2. @-mention the coordinator definition: ${pc.bold(coordinatorRel)}
8364
+
8365
+ To use the CLI lane instead, install agy / put it on PATH, or set AGY_BIN_PATH.`
8366
+ ),
8367
+ "Antigravity (desktop route)"
8368
+ );
8369
+ outro(pc.yellow("No session launched."));
8370
+ return;
8371
+ }
8372
+ const plan = launcher.planActivation({
8373
+ bundleName: bundle,
8374
+ workspace: resolution.workspace,
8375
+ scope: resolution.scope,
8376
+ report: probeReport,
8377
+ prompt,
8378
+ orchestrator: bundleDef?.orchestrator
8379
+ });
8380
+ if (options.dryRun) {
8381
+ note(
8382
+ `Bundle: ${plan.bundleName}
8383
+ Tier: ${bundleDef?.tier ?? "not declared (Tier-1 domain default)"}
8384
+ Scope: ${plan.scope}
8385
+ Workspace: ${plan.workspace}
8386
+ Executable: ${plan.executable}
8387
+ Argv: ${plan.argv.map((a) => a.includes(" ") || a.includes("\n") ? `"${a.replace(/\n/g, "\\n")}"` : a).join(" ")}`,
8388
+ "Antigravity Activation Plan (dry run)"
8389
+ );
8390
+ outro(pc.yellow("Dry run complete. No processes launched."));
8391
+ return;
8392
+ }
8393
+ const coordinatorDirEntry = path22.join(resolution.workspace, ".agents", "agents", coordinatorName, "agent.md");
8394
+ const coordinatorPath = path22.join(resolution.workspace, ".agents", "agents", `${coordinatorName}.md`);
8395
+ const coordinatorFound = await fs20.pathExists(coordinatorPath) || await fs20.pathExists(coordinatorDirEntry);
8396
+ if (!coordinatorFound) {
8397
+ outro(
8398
+ pc.red(
8399
+ `Canonical coordinator "${coordinatorRel}" was not found in ${resolution.workspace}.
8400
+ Run 'agents add ${bundle}' before starting an Antigravity session.`
8401
+ )
8402
+ );
8403
+ process.exit(1);
8404
+ }
8405
+ if (!probeReport.promptInteractive) {
8406
+ note(
8407
+ pc.yellow("The capability probe could not confirm --prompt-interactive from `agy --help`; the opening prompt is still passed as a single argv element."),
8408
+ "Antigravity CLI"
8409
+ );
8410
+ }
8411
+ note(
8412
+ `Host: ${pc.cyan("antigravity")}
8413
+ Executable: ${plan.executable}
8414
+ Workspace: ${plan.workspace}
8415
+ Agent: ${coordinatorName}`,
8416
+ "Starting Antigravity Session"
8417
+ );
8418
+ await launcher.launch(plan);
8419
+ outro(pc.green(`\u2714 Antigravity session finished.`));
7286
8420
  }
7287
8421
  function resolveClaudeTeamsPosture(teamsOption, tier) {
7288
8422
  if (teamsOption === true) return { active: true, reason: "explicit", tier };
@@ -7320,8 +8454,8 @@ async function runClaudeStart(bundle, prompt, options) {
7320
8454
  const bundleDef = await registry.getBundle(bundle);
7321
8455
  const teamsPosture = resolveClaudeTeamsPosture(options.teams, bundleDef?.tier);
7322
8456
  const teamsActive = teamsPosture.active;
7323
- const manifestAvailable = await fs15.pathExists(resolution.manifestPath);
7324
- const pluginDir = options.plugin ? path17.join(".agents", "plugins", bundle).split(path17.sep).join("/") : void 0;
8457
+ const manifestAvailable = await fs20.pathExists(resolution.manifestPath);
8458
+ const pluginDir = options.plugin ? path22.join(".agents", "plugins", bundle).split(path22.sep).join("/") : void 0;
7325
8459
  const plan = launcher.planActivation({
7326
8460
  bundleName: bundle,
7327
8461
  workspace: resolution.workspace,
@@ -7377,7 +8511,7 @@ Plugin dir: ${pluginDir || "(none)"}`,
7377
8511
  "Starting Claude Code Session"
7378
8512
  );
7379
8513
  const coordinatorRel = `.claude/agents/${ClaudeLauncher.resolveCoordinatorName(bundleDef?.orchestrator)}.md`;
7380
- if (!await fs15.pathExists(path17.join(resolution.workspace, coordinatorRel))) {
8514
+ if (!await fs20.pathExists(path22.join(resolution.workspace, coordinatorRel))) {
7381
8515
  outro(
7382
8516
  pc.red(
7383
8517
  `Claude projection "${coordinatorRel}" was not found in ${resolution.workspace}.
@@ -7399,6 +8533,15 @@ function renderClaudeCapabilityBlock(capability) {
7399
8533
  console.log(` Agent Teams (experimental): ${capability.agentTeamsExperimental ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported")}`);
7400
8534
  console.log(` Subagent hand-off (SubagentHandback): ${capability.subagentHandback ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Needs v2.1.271+ (auto mode)")}`);
7401
8535
  }
8536
+ function printDeclaredDeltas(report, host) {
8537
+ if (!report.declaredDeltas || report.declaredDeltas.length === 0) return;
8538
+ console.log(pc.bold(pc.cyan(`Declared Deltas for --host ${host} (docs/host-primitive-matrix.md):`)));
8539
+ for (const delta of report.declaredDeltas) {
8540
+ const tag = delta.disposition === "unsupported" ? pc.red("unsupported") : pc.yellow("degraded");
8541
+ console.log(` \u2022 ${pc.bold(delta.feature)} (${tag}): ${delta.rationale}`);
8542
+ }
8543
+ console.log();
8544
+ }
7402
8545
  function renderClaudeActivationPlan(plan, report, recordedFanout, posture) {
7403
8546
  const envEntries = Object.keys(plan.env);
7404
8547
  const pluginFlagIndex = plan.argv.indexOf("--plugin-dir");