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.
- package/README.md +507 -405
- package/dist/cli.js +1772 -629
- package/dist/cli.js.map +1 -1
- package/package.json +10 -4
- package/registry/agents/orchestrator-business.md +21 -5
- package/registry/agents/orchestrator-design.md +21 -5
- package/registry/agents/orchestrator-digital-agency.md +9 -1
- package/registry/agents/orchestrator-engineering.md +12 -5
- package/registry/agents/orchestrator-marketing.md +14 -5
- package/registry/agents/orchestrator-research.md +14 -5
- package/registry/agents/orchestrator-security.md +15 -5
- package/registry/agents/orchestrator-system-architecture.md +14 -5
- package/registry/agents/orchestrator-universal.md +11 -2
- package/registry/agents/subagent-accessibility-lead.md +49 -3
- package/registry/agents/subagent-ai-model-architect.md +48 -2
- package/registry/agents/subagent-android-architect.md +53 -3
- package/registry/agents/subagent-appsec-penetration-tester.md +27 -1
- package/registry/agents/subagent-backend-architect.md +63 -232
- package/registry/agents/subagent-business-panel-experts.md +5 -1
- package/registry/agents/subagent-cloud-infrastructure-architect.md +23 -1
- package/registry/agents/subagent-cloud-security-architect.md +7 -1
- package/registry/agents/subagent-code-reviewer.md +31 -1
- package/registry/agents/subagent-compliance-grc-specialist.md +7 -2
- package/registry/agents/subagent-cross-platform-specialist.md +54 -3
- package/registry/agents/subagent-data-engineer.md +53 -3
- package/registry/agents/subagent-database-administrator.md +7 -1
- package/registry/agents/subagent-deep-research.md +5 -1
- package/registry/agents/subagent-design-ops-lead.md +5 -1
- package/registry/agents/subagent-design-researcher.md +5 -1
- package/registry/agents/subagent-design-systems-architect.md +5 -1
- package/registry/agents/subagent-designer-toolkit-expert.md +5 -1
- package/registry/agents/subagent-devops-engineer.md +68 -288
- package/registry/agents/subagent-distributed-systems-architect.md +54 -3
- package/registry/agents/subagent-e2e-tester.md +52 -3
- package/registry/agents/subagent-financial-analyst.md +5 -1
- package/registry/agents/subagent-finops-cost-engineer.md +5 -1
- package/registry/agents/subagent-frontend-architect.md +54 -225
- package/registry/agents/subagent-interaction-designer.md +5 -1
- package/registry/agents/subagent-ios-architect.md +53 -3
- package/registry/agents/subagent-legal-contract-analyst.md +5 -1
- package/registry/agents/subagent-lifecycle-email-specialist.md +5 -1
- package/registry/agents/subagent-literature-patent-analyst.md +5 -1
- package/registry/agents/subagent-market-intelligence-analyst.md +5 -1
- package/registry/agents/subagent-marketing-campaign-specialist.md +6 -2
- package/registry/agents/subagent-marketing-content-strategist.md +6 -2
- package/registry/agents/subagent-marketing-conversion-specialist.md +6 -2
- package/registry/agents/subagent-marketing-creative-designer.md +6 -2
- package/registry/agents/subagent-marketing-growth-strategist.md +6 -2
- package/registry/agents/subagent-ml-platform-engineer.md +48 -2
- package/registry/agents/subagent-operations-strategist.md +5 -1
- package/registry/agents/subagent-paid-acquisition-specialist.md +5 -1
- package/registry/agents/subagent-plg-strategist.md +5 -1
- package/registry/agents/subagent-prototype-tester.md +5 -1
- package/registry/agents/subagent-qa-automation-lead.md +35 -78
- package/registry/agents/subagent-repo-index.md +27 -1
- package/registry/agents/subagent-security-engineer.md +37 -1
- package/registry/agents/subagent-seo-specialist.md +6 -2
- package/registry/agents/subagent-socratic-mentor.md +5 -1
- package/registry/agents/subagent-statistical-analyst.md +5 -1
- package/registry/agents/subagent-sysops-sre-lead.md +7 -1
- package/registry/agents/subagent-system-architect.md +6 -1
- package/registry/agents/subagent-ui-designer.md +5 -1
- package/registry/agents/subagent-ux-strategist.md +5 -1
- package/registry/bundles.json +89 -17
- package/registry/core/orchestrator-engineering.core.md +1 -0
- package/registry/core/subagent-backend-architect.core.md +20 -22
- package/registry/core/subagent-code-reviewer.core.md +1 -0
- package/registry/core/subagent-frontend-architect.core.md +8 -6
- package/registry/core/subagent-repo-index.core.md +1 -0
- package/registry/skills/ai-prototype-refactoring/SKILL.md +24 -0
- package/registry/skills/ai-prototype-refactoring/references/backend-frontend-devops-exemplars.md +104 -0
- package/registry/skills/azure-infrastructure-bicep/SKILL.md +24 -0
- package/registry/skills/azure-infrastructure-bicep/references/backend-frontend-devops-exemplars.md +173 -0
- package/registry/skills/banner-design/SKILL.md +170 -0
- package/registry/skills/banner-design/references/banner-sizes-and-styles.md +66 -0
- package/registry/skills/brand-identity/SKILL.md +212 -0
- package/registry/skills/brand-identity/references/approval-checklist.md +17 -0
- package/registry/skills/brand-identity/references/asset-organization.md +30 -0
- package/registry/skills/brand-identity/references/brand-guideline-template.md +57 -0
- package/registry/skills/brand-identity/references/color-palette-management.md +37 -0
- package/registry/skills/brand-identity/references/consistency-checklist.md +26 -0
- package/registry/skills/brand-identity/references/logo-usage-rules.md +23 -0
- package/registry/skills/brand-identity/references/messaging-framework.md +22 -0
- package/registry/skills/brand-identity/references/typography-specifications.md +29 -0
- package/registry/skills/brand-identity/references/visual-identity.md +23 -0
- package/registry/skills/brand-identity/references/voice-framework.md +27 -0
- package/registry/skills/brand-identity/scripts/extract-colors.mjs +104 -0
- package/registry/skills/brand-identity/scripts/inject-brand-context.mjs +112 -0
- package/registry/skills/brand-identity/scripts/lib/confirm-gate.mjs +28 -0
- package/registry/skills/brand-identity/scripts/sync-brand-to-tokens.mjs +205 -0
- package/registry/skills/brand-identity/scripts/validate-asset.mjs +137 -0
- package/registry/skills/clickhouse-architecture-advisor/SKILL.md +202 -0
- package/registry/skills/codeql-scanning/LICENSE +427 -0
- package/registry/skills/codeql-scanning/NOTICE.md +31 -0
- package/registry/skills/codeql-scanning/SKILL.md +136 -0
- package/registry/skills/codeql-scanning/references/build-fixes.md +105 -0
- package/registry/skills/codeql-scanning/references/diagnostic-query-templates.md +325 -0
- package/registry/skills/codeql-scanning/references/extension-yaml-format.md +202 -0
- package/registry/skills/codeql-scanning/references/important-only-suite.md +119 -0
- package/registry/skills/codeql-scanning/references/language-details.md +220 -0
- package/registry/skills/codeql-scanning/references/macos-arm64e-workaround.md +201 -0
- package/registry/skills/codeql-scanning/references/performance-tuning.md +81 -0
- package/registry/skills/codeql-scanning/references/quality-assessment.md +197 -0
- package/registry/skills/codeql-scanning/references/ruleset-catalog.md +68 -0
- package/registry/skills/codeql-scanning/references/run-all-suite.md +106 -0
- package/registry/skills/codeql-scanning/references/sarif-processing.md +82 -0
- package/registry/skills/codeql-scanning/references/threat-models.md +36 -0
- package/registry/skills/codeql-scanning/references/upstream-method.md +288 -0
- package/registry/skills/codeql-scanning/references/workflow-build-database.md +338 -0
- package/registry/skills/codeql-scanning/references/workflow-create-data-extensions.md +275 -0
- package/registry/skills/codeql-scanning/references/workflow-run-analysis.md +327 -0
- package/registry/skills/codeql-scanning/scripts/build_log.py +91 -0
- package/registry/skills/codeql-scanning/scripts/check_db_quality.py +366 -0
- package/registry/skills/codeql-scanning/scripts/find_databases.py +90 -0
- package/registry/skills/codeql-scanning/scripts/generate_suite.py +101 -0
- package/registry/skills/codeql-scanning/scripts/verify_query_suite.py +86 -0
- package/registry/skills/edge-security-audit/SKILL.md +225 -0
- package/registry/skills/expo-cicd-workflows/SKILL.md +228 -0
- package/registry/skills/hf-managed-jobs/SKILL.md +198 -0
- package/registry/skills/hf-model-training/SKILL.md +204 -0
- package/registry/skills/mutation-testing/LICENSE +427 -0
- package/registry/skills/mutation-testing/NOTICE.md +31 -0
- package/registry/skills/mutation-testing/SKILL.md +122 -0
- package/registry/skills/mutation-testing/references/blockchain-patterns.md +103 -0
- package/registry/skills/mutation-testing/references/equivalent-mutants.md +101 -0
- package/registry/skills/mutation-testing/references/input-formats.md +84 -0
- package/registry/skills/mutation-testing/references/optimization-strategies.md +327 -0
- package/registry/skills/mutation-testing/references/report-template.md +140 -0
- package/registry/skills/mutation-testing/references/severity-classification.md +132 -0
- package/registry/skills/mutation-testing/references/upstream-method.md +105 -0
- package/registry/skills/mutation-testing/references/workflow-analyzing-results.md +123 -0
- package/registry/skills/mutation-testing/references/workflow-bug-hunter.md +139 -0
- package/registry/skills/mutation-testing/references/workflow-configuration.md +338 -0
- package/registry/skills/playwright-best-practices/SKILL.md +5 -0
- package/registry/skills/playwright-best-practices/references/conversion-funnel-test.md +79 -0
- package/registry/skills/postgres-best-practices/SKILL.md +205 -0
- package/registry/skills/property-based-testing/LICENSE +427 -0
- package/registry/skills/property-based-testing/NOTICE.md +31 -0
- package/registry/skills/property-based-testing/SKILL.md +119 -0
- package/registry/skills/property-based-testing/references/generating.md +82 -0
- package/registry/skills/property-based-testing/references/interpreting-failures.md +83 -0
- package/registry/skills/property-based-testing/references/libraries.md +80 -0
- package/registry/skills/property-based-testing/references/refactoring.md +123 -0
- package/registry/skills/property-based-testing/references/reviewing.md +81 -0
- package/registry/skills/property-based-testing/references/upstream-method.md +65 -0
- package/registry/skills/react-best-practices/SKILL.md +5 -0
- package/registry/skills/react-best-practices/references/nextjs-server-actions-and-isr.md +85 -0
- package/registry/skills/sarif-triage/LICENSE +427 -0
- package/registry/skills/sarif-triage/NOTICE.md +31 -0
- package/registry/skills/sarif-triage/SKILL.md +117 -0
- package/registry/skills/sarif-triage/references/jq-queries.md +210 -0
- package/registry/skills/sarif-triage/references/upstream-method.md +490 -0
- package/registry/skills/sarif-triage/scripts/sarif_helpers.py +400 -0
- package/registry/skills/security-best-practices/LICENSE +201 -0
- package/registry/skills/security-best-practices/NOTICE.md +30 -0
- package/registry/skills/security-best-practices/SKILL.md +116 -0
- package/registry/skills/security-best-practices/references/golang-general-backend-security.md +829 -0
- package/registry/skills/security-best-practices/references/javascript-express-web-server-security.md +1161 -0
- package/registry/skills/security-best-practices/references/javascript-general-web-frontend-security.md +750 -0
- package/registry/skills/security-best-practices/references/javascript-jquery-web-frontend-security.md +681 -0
- package/registry/skills/security-best-practices/references/javascript-typescript-nextjs-web-server-security.md +1147 -0
- package/registry/skills/security-best-practices/references/javascript-typescript-react-web-frontend-security.md +993 -0
- package/registry/skills/security-best-practices/references/javascript-typescript-vue-web-frontend-security.md +794 -0
- package/registry/skills/security-best-practices/references/python-django-web-server-security.md +885 -0
- package/registry/skills/security-best-practices/references/python-fastapi-web-server-security.md +1039 -0
- package/registry/skills/security-best-practices/references/python-flask-web-server-security.md +708 -0
- package/registry/skills/security-best-practices/references/upstream-method.md +86 -0
- package/registry/skills/security-diff-review/LICENSE +427 -0
- package/registry/skills/security-diff-review/NOTICE.md +31 -0
- package/registry/skills/security-diff-review/SKILL.md +125 -0
- package/registry/skills/security-diff-review/references/adversarial.md +206 -0
- package/registry/skills/security-diff-review/references/methodology.md +237 -0
- package/registry/skills/security-diff-review/references/patterns.md +303 -0
- package/registry/skills/security-diff-review/references/reporting.md +372 -0
- package/registry/skills/security-diff-review/references/upstream-method.md +222 -0
- package/registry/skills/semgrep-scanning/LICENSE +427 -0
- package/registry/skills/semgrep-scanning/NOTICE.md +31 -0
- package/registry/skills/semgrep-scanning/SKILL.md +139 -0
- package/registry/skills/semgrep-scanning/references/rulesets.md +165 -0
- package/registry/skills/semgrep-scanning/references/scan-modes.md +153 -0
- package/registry/skills/semgrep-scanning/references/upstream-method.md +262 -0
- package/registry/skills/semgrep-scanning/references/workflow-scan-workflow.md +467 -0
- package/registry/skills/semgrep-scanning/scripts/merge_sarif.py +339 -0
- package/registry/skills/semgrep-scanning/scripts/run_scans.py +482 -0
- package/registry/skills/sentry-incident-triage/SKILL.md +193 -0
- package/registry/skills/supabase-backend-architecture/SKILL.md +24 -0
- package/registry/skills/supabase-backend-architecture/references/backend-frontend-devops-exemplars.md +140 -0
- package/registry/skills/supply-chain-risk-audit/LICENSE +427 -0
- package/registry/skills/supply-chain-risk-audit/NOTICE.md +31 -0
- package/registry/skills/supply-chain-risk-audit/SKILL.md +119 -0
- package/registry/skills/supply-chain-risk-audit/references/upstream-method.md +123 -0
- package/registry/skills/supply-chain-risk-audit/scripts/collect.py +1568 -0
- package/registry/skills/supply-chain-risk-audit/scripts/model.py +402 -0
- package/registry/skills/supply-chain-risk-audit/scripts/pyproject.toml +20 -0
- package/registry/skills/supply-chain-risk-audit/scripts/render.py +809 -0
- package/registry/skills/supply-chain-risk-audit/scripts/sources.py +683 -0
- package/registry/skills/supply-chain-risk-audit/scripts/uv.lock +12 -0
- package/registry/skills/terraform-style-guide/LICENSE +373 -0
- package/registry/skills/terraform-style-guide/NOTICE.md +31 -0
- package/registry/skills/terraform-style-guide/SKILL.md +126 -0
- package/registry/skills/terraform-style-guide/references/security.md +167 -0
- package/registry/skills/terraform-style-guide/references/style-conventions.md +312 -0
- package/registry/skills/terraform-test-patterns/LICENSE +373 -0
- package/registry/skills/terraform-test-patterns/NOTICE.md +31 -0
- package/registry/skills/terraform-test-patterns/SKILL.md +133 -0
- package/registry/skills/terraform-test-patterns/references/ci-cd.md +83 -0
- package/registry/skills/terraform-test-patterns/references/examples.md +317 -0
- package/registry/skills/terraform-test-patterns/references/mock-providers.md +174 -0
- package/registry/skills/terraform-test-patterns/references/test-syntax.md +445 -0
- package/registry/skills/threat-modeling/LICENSE +201 -0
- package/registry/skills/threat-modeling/NOTICE.md +31 -0
- package/registry/skills/threat-modeling/SKILL.md +136 -0
- package/registry/skills/threat-modeling/references/prompt-template.md +258 -0
- package/registry/skills/threat-modeling/references/security-controls-and-assets.md +35 -0
- package/registry/skills/threat-modeling/references/upstream-method.md +81 -0
- package/registry/skills/turso-distributed-sqlite/SKILL.md +24 -0
- package/registry/skills/turso-distributed-sqlite/references/backend-frontend-devops-exemplars.md +92 -0
- package/registry/skills/ux-writing/SKILL.md +183 -0
- package/registry/skills/ux-writing/references/interface-patterns.md +41 -0
- package/registry/skills/ux-writing/references/localization-accessibility.md +31 -0
- package/registry/skills/ux-writing/references/research-measurement.md +27 -0
- package/registry/skills/ux-writing/references/source-runtime-workflow.md +31 -0
- package/registry/skills/ux-writing/references/voice-tone-terminology.md +26 -0
- package/registry/skills/variant-analysis/LICENSE +427 -0
- package/registry/skills/variant-analysis/NOTICE.md +31 -0
- package/registry/skills/variant-analysis/SKILL.md +119 -0
- package/registry/skills/variant-analysis/assets/codeql/cpp.ql +119 -0
- package/registry/skills/variant-analysis/assets/codeql/go.ql +69 -0
- package/registry/skills/variant-analysis/assets/codeql/java.ql +71 -0
- package/registry/skills/variant-analysis/assets/codeql/javascript.ql +63 -0
- package/registry/skills/variant-analysis/assets/codeql/python.ql +80 -0
- package/registry/skills/variant-analysis/assets/semgrep/cpp.yaml +98 -0
- package/registry/skills/variant-analysis/assets/semgrep/go.yaml +63 -0
- package/registry/skills/variant-analysis/assets/semgrep/java.yaml +61 -0
- package/registry/skills/variant-analysis/assets/semgrep/javascript.yaml +60 -0
- package/registry/skills/variant-analysis/assets/semgrep/python.yaml +72 -0
- package/registry/skills/variant-analysis/references/reporting.md +41 -0
- package/registry/skills/variant-analysis/references/root-cause.md +101 -0
- package/registry/skills/variant-analysis/references/searching.md +162 -0
- package/registry/skills/variant-analysis/references/triage.md +95 -0
- package/registry/skills/variant-analysis/references/upstream-method.md +74 -0
- package/registry/skills/variant-analysis/references/variant-report-template.md +78 -0
- package/registry/skills/vercel-deploy-best-practices/SKILL.md +24 -0
- package/registry/skills/vercel-deploy-best-practices/references/backend-frontend-devops-exemplars.md +109 -0
- 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
|
|
8
|
-
import
|
|
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
|
|
343
|
-
import
|
|
344
|
-
import
|
|
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 =
|
|
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.
|
|
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: "
|
|
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).
|
|
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
|
|
1737
|
-
for (const
|
|
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}/${
|
|
1742
|
-
relPath: `.claude/skills/${normalized}/${
|
|
1743
|
-
sourceFilePath: path5.join(skillDir,
|
|
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
|
|
1871
|
-
return "sha256:" +
|
|
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
|
|
2234
|
+
if (await fs7.pathExists(lockfilePath)) {
|
|
1875
2235
|
try {
|
|
1876
|
-
return await
|
|
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
|
|
1920
|
-
if (await
|
|
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
|
|
2282
|
+
await fs7.remove(dest);
|
|
1923
2283
|
}
|
|
1924
2284
|
}
|
|
1925
2285
|
if (method === "symlink") {
|
|
1926
2286
|
try {
|
|
1927
|
-
const stat = await
|
|
2287
|
+
const stat = await fs7.stat(src);
|
|
1928
2288
|
const type = stat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
1929
|
-
await
|
|
2289
|
+
await fs7.ensureSymlink(src, dest, type);
|
|
1930
2290
|
return "symlink";
|
|
1931
2291
|
} catch {
|
|
1932
|
-
await
|
|
2292
|
+
await fs7.copy(src, dest, { overwrite: true });
|
|
1933
2293
|
return "copy";
|
|
1934
2294
|
}
|
|
1935
2295
|
}
|
|
1936
|
-
await
|
|
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
|
|
1945
|
-
await
|
|
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:
|
|
2313
|
+
return { agentsTarget, root: workspaceRootOf(agentsTarget) };
|
|
1954
2314
|
}
|
|
1955
2315
|
toPosix(p) {
|
|
1956
|
-
return p.split(
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
2084
|
-
if (await
|
|
2085
|
-
await
|
|
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 =
|
|
2098
|
-
if (await
|
|
2099
|
-
const existing = await
|
|
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
|
|
2119
|
-
await
|
|
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 =
|
|
2647
|
+
let dir = path9.dirname(path9.join(workspaceRoot, projPath));
|
|
2188
2648
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
2189
2649
|
try {
|
|
2190
|
-
const entries = await
|
|
2650
|
+
const entries = await fs7.readdir(dir);
|
|
2191
2651
|
if (entries.length > 0) break;
|
|
2192
|
-
await
|
|
2652
|
+
await fs7.rmdir(dir);
|
|
2193
2653
|
} catch {
|
|
2194
2654
|
break;
|
|
2195
2655
|
}
|
|
2196
|
-
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
|
|
2666
|
+
if (!targetLockfile && await fs7.pathExists(subPaths.lockfile)) {
|
|
2207
2667
|
try {
|
|
2208
|
-
targetLockfile = await
|
|
2668
|
+
targetLockfile = await fs7.readJson(subPaths.lockfile);
|
|
2209
2669
|
lockfileLoadedFromDisk = true;
|
|
2210
2670
|
} catch {
|
|
2211
2671
|
}
|
|
2212
2672
|
}
|
|
2213
|
-
if (await
|
|
2214
|
-
const entries = await
|
|
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 =
|
|
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 =
|
|
2223
|
-
const targetSkillFile =
|
|
2224
|
-
if (!await
|
|
2225
|
-
await
|
|
2226
|
-
const legacyContent = await
|
|
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
|
|
2707
|
+
await fs7.writeFile(targetSkillFile, skillContent, "utf8");
|
|
2248
2708
|
}
|
|
2249
|
-
await
|
|
2709
|
+
await fs7.remove(legacyPath).catch(() => {
|
|
2250
2710
|
});
|
|
2251
2711
|
migratedCount++;
|
|
2252
2712
|
if (targetLockfile) {
|
|
2253
|
-
const legacyRel = this.toPosix(
|
|
2254
|
-
const newRel = this.toPosix(
|
|
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
|
|
2727
|
+
const remaining = await fs7.readdir(subPaths.workflowsDir).catch(() => []);
|
|
2268
2728
|
if (remaining.length === 0) {
|
|
2269
|
-
await
|
|
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
|
|
2286
|
-
await
|
|
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
|
|
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 =
|
|
2325
|
-
infos.push({ host, path: this.toPosix(
|
|
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 =
|
|
2355
|
-
if (await
|
|
2356
|
-
const existing = await
|
|
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",
|
|
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 =
|
|
2408
|
-
if (!await
|
|
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
|
|
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 =
|
|
2442
|
-
const projPath = this.toPosix(
|
|
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
|
|
2451
|
-
const existing = await
|
|
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(
|
|
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
|
|
2496
|
-
|
|
2497
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
2515
|
-
await
|
|
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
|
|
2979
|
+
await fs7.ensureDir(subPaths.workflowsDir);
|
|
2518
2980
|
}
|
|
2519
|
-
await
|
|
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 =
|
|
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 =
|
|
2544
|
-
const dest =
|
|
2545
|
-
if (await
|
|
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 =
|
|
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,
|
|
3016
|
+
const actualMethod = await this.deployFile(src, dest, iterMethod, options.force);
|
|
2553
3017
|
const hash = await this.calculateHash(src);
|
|
2554
|
-
const relPath = this.toPosix(
|
|
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 =
|
|
2572
|
-
const dest =
|
|
2573
|
-
const actualMethod = await this.deployFile(src, dest,
|
|
2574
|
-
const skillFile =
|
|
2575
|
-
if (await
|
|
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(
|
|
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
|
|
2594
|
-
const abs =
|
|
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(
|
|
2600
|
-
if (abs ===
|
|
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 =
|
|
2620
|
-
const dest =
|
|
2621
|
-
const actualMethod = await this.deployFile(src, dest,
|
|
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(
|
|
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 =
|
|
2642
|
-
const dest =
|
|
2643
|
-
if (await
|
|
2644
|
-
const actualMethod = await this.deployFile(src, dest,
|
|
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(
|
|
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 &&
|
|
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
|
-
|
|
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
|
|
2683
|
-
import
|
|
2684
|
-
import
|
|
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
|
|
2692
|
-
return "sha256:" +
|
|
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
|
|
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
|
|
2708
|
-
return `sha256:${
|
|
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 =
|
|
3187
|
+
let dir = path10.dirname(path10.join(workspaceRoot, projPath));
|
|
2718
3188
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
2719
3189
|
try {
|
|
2720
|
-
const entries = await
|
|
3190
|
+
const entries = await fs8.readdir(dir);
|
|
2721
3191
|
if (entries.length > 0) break;
|
|
2722
|
-
await
|
|
3192
|
+
await fs8.rmdir(dir);
|
|
2723
3193
|
} catch {
|
|
2724
3194
|
break;
|
|
2725
3195
|
}
|
|
2726
|
-
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 =
|
|
2736
|
-
if (!await
|
|
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
|
|
3222
|
+
const lstat = await fs8.lstat(absPath);
|
|
2753
3223
|
if (lstat.isSymbolicLink()) {
|
|
2754
|
-
await
|
|
3224
|
+
await fs8.unlink(absPath);
|
|
2755
3225
|
return;
|
|
2756
3226
|
}
|
|
2757
3227
|
} catch {
|
|
2758
3228
|
}
|
|
2759
|
-
await
|
|
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 =
|
|
2789
|
-
if (!await
|
|
2790
|
-
await
|
|
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 =
|
|
2813
|
-
if (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 =
|
|
3286
|
+
const skillDir = path10.join(targetDir, parts[0], parts[1]);
|
|
2817
3287
|
try {
|
|
2818
|
-
const lstat = await
|
|
3288
|
+
const lstat = await fs8.lstat(skillDir);
|
|
2819
3289
|
if (lstat.isSymbolicLink()) {
|
|
2820
|
-
await
|
|
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
|
|
3298
|
+
const lstat = await fs8.lstat(fullPath);
|
|
2829
3299
|
if (lstat.isSymbolicLink()) {
|
|
2830
|
-
await
|
|
3300
|
+
await fs8.unlink(fullPath);
|
|
2831
3301
|
return;
|
|
2832
3302
|
}
|
|
2833
3303
|
} catch {
|
|
2834
3304
|
}
|
|
2835
|
-
await
|
|
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
|
|
3318
|
+
if (!await fs8.pathExists(subPaths.lockfile)) {
|
|
2849
3319
|
continue;
|
|
2850
3320
|
}
|
|
2851
|
-
const lockfile = await
|
|
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 =
|
|
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 =
|
|
2927
|
-
const existedOnDisk = await
|
|
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 =
|
|
2973
|
-
if (await
|
|
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
|
|
2992
|
-
const entries = await
|
|
3461
|
+
if (await fs8.pathExists(dir)) {
|
|
3462
|
+
const entries = await fs8.readdir(dir).catch(() => []);
|
|
2993
3463
|
if (entries.length === 0) {
|
|
2994
|
-
await
|
|
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
|
-
|
|
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 =
|
|
3046
|
-
if (await
|
|
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
|
|
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
|
|
3084
|
-
import
|
|
3085
|
-
import
|
|
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 =
|
|
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 =
|
|
3585
|
+
const rel = path11.relative(cwd, targetDir);
|
|
3097
3586
|
prettyPath = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
3098
3587
|
} else if (targetDir.startsWith(home)) {
|
|
3099
|
-
const rel =
|
|
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:
|
|
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 =
|
|
3635
|
+
const home = os4.homedir();
|
|
3147
3636
|
for (const scope of scopes) {
|
|
3148
|
-
const canonicalDir =
|
|
3637
|
+
const canonicalDir = resolveStateDir(scope, options.targetDir, { cwd });
|
|
3149
3638
|
let displayLoc = canonicalDir;
|
|
3150
3639
|
if (canonicalDir.startsWith(cwd)) {
|
|
3151
|
-
const rel =
|
|
3640
|
+
const rel = path11.relative(cwd, canonicalDir);
|
|
3152
3641
|
displayLoc = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
3153
3642
|
} else if (canonicalDir.startsWith(home)) {
|
|
3154
|
-
const rel =
|
|
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 =
|
|
3167
|
-
if (!await
|
|
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
|
|
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
|
|
3323
|
-
import
|
|
3324
|
-
import
|
|
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
|
|
3348
|
-
return "sha256:" +
|
|
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
|
|
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
|
|
3363
|
-
return `sha256:${
|
|
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
|
-
|
|
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 =
|
|
3378
|
-
if (await
|
|
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 =
|
|
3882
|
+
let dir = path12.dirname(path12.join(workspaceRoot, projPath));
|
|
3394
3883
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
3395
3884
|
try {
|
|
3396
|
-
const entries = await
|
|
3885
|
+
const entries = await fs10.readdir(dir);
|
|
3397
3886
|
if (entries.length > 0) break;
|
|
3398
|
-
await
|
|
3887
|
+
await fs10.rmdir(dir);
|
|
3399
3888
|
} catch {
|
|
3400
3889
|
break;
|
|
3401
3890
|
}
|
|
3402
|
-
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 =
|
|
3449
|
-
if (await
|
|
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
|
|
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 =
|
|
3513
|
-
if (await
|
|
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 =
|
|
3540
|
-
if (!await
|
|
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
|
|
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 =
|
|
3550
|
-
if (await
|
|
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
|
-
|
|
4068
|
+
workspaceRootOf(record.targetDir),
|
|
3580
4069
|
lockfile
|
|
3581
4070
|
);
|
|
3582
4071
|
if (migratedGenerativeUi.length > 0) {
|
|
3583
|
-
await
|
|
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
|
|
3598
|
-
const updatedLockfile = await
|
|
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
|
|
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
|
|
3629
|
-
import
|
|
3630
|
-
import
|
|
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
|
|
3635
|
-
import
|
|
4123
|
+
import path14 from "path";
|
|
4124
|
+
import fs12 from "fs-extra";
|
|
3636
4125
|
|
|
3637
4126
|
// src/core/cline-capabilities.ts
|
|
3638
|
-
import
|
|
3639
|
-
import
|
|
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 &&
|
|
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 =
|
|
3711
|
-
if (
|
|
3712
|
-
const adjacentWrapper1 =
|
|
3713
|
-
const adjacentWrapper2 =
|
|
3714
|
-
if (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
3747
|
-
if (
|
|
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 &&
|
|
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 =
|
|
3869
|
-
if (
|
|
3870
|
-
const adjacentWrapper1 =
|
|
3871
|
-
const adjacentWrapper2 =
|
|
3872
|
-
if (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
3905
|
-
if (
|
|
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/
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
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
|
-
*
|
|
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
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
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 =
|
|
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
|
|
4108
|
-
const agentsDirExists = await
|
|
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
|
|
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
|
|
4126
|
-
const agentFiles = await
|
|
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
|
|
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
|
|
4147
|
-
const remainingWorkflowFiles = (await
|
|
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 =
|
|
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 =
|
|
4165
|
-
if (!await
|
|
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 =
|
|
4190
|
-
if (!await
|
|
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 =
|
|
4234
|
-
if (!await
|
|
5059
|
+
const absPath = path16.join(workspaceRoot, projRelPath);
|
|
5060
|
+
if (!await fs14.pathExists(absPath)) continue;
|
|
4235
5061
|
if (!proj.managedMarker) continue;
|
|
4236
|
-
const content = await
|
|
5062
|
+
const content = await fs14.readFile(absPath, "utf8");
|
|
4237
5063
|
if (!HostProjector.hasManagedMarker(content)) continue;
|
|
4238
5064
|
if (proj.hash) {
|
|
4239
|
-
const diskHash = `sha256:${
|
|
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
|
|
4317
|
-
import
|
|
4318
|
-
import
|
|
4319
|
-
import
|
|
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 =
|
|
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 =
|
|
5256
|
+
workspace = os6.homedir();
|
|
4357
5257
|
} else {
|
|
4358
5258
|
workspace = cwd;
|
|
4359
|
-
targetDir =
|
|
5259
|
+
targetDir = path17.join(cwd, ".agents");
|
|
4360
5260
|
scope = "project";
|
|
4361
5261
|
}
|
|
4362
|
-
const lockfilePath =
|
|
4363
|
-
if (!await
|
|
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 =
|
|
4367
|
-
if (await
|
|
4368
|
-
const gLock = await
|
|
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
|
|
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 =
|
|
4383
|
-
if (!hasClineFanout || !await
|
|
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
|
|
4484
|
-
import
|
|
4485
|
-
import
|
|
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(
|
|
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 =
|
|
5417
|
+
targetDir = resolveStateDir("global");
|
|
4518
5418
|
scope = "global";
|
|
4519
|
-
workspace =
|
|
5419
|
+
workspace = os7.homedir();
|
|
4520
5420
|
} else {
|
|
4521
5421
|
workspace = cwd;
|
|
4522
|
-
targetDir =
|
|
5422
|
+
targetDir = resolveStateDir("project", void 0, { cwd });
|
|
4523
5423
|
scope = "project";
|
|
4524
5424
|
}
|
|
4525
|
-
const lockfilePath =
|
|
4526
|
-
if (!await
|
|
5425
|
+
const lockfilePath = path18.join(targetDir, "agents-united.json");
|
|
5426
|
+
if (!await fs16.pathExists(lockfilePath)) {
|
|
4527
5427
|
if (!isGlobal) {
|
|
4528
|
-
const globalTarget =
|
|
4529
|
-
const globalLockfile =
|
|
4530
|
-
if (await
|
|
4531
|
-
const gLock = await
|
|
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
|
|
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 =
|
|
4546
|
-
if (!hasClaudeFanout && !await
|
|
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/
|
|
4658
|
-
import
|
|
4659
|
-
import
|
|
4660
|
-
import
|
|
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/
|
|
4663
|
-
import
|
|
4664
|
-
import
|
|
4665
|
-
|
|
4666
|
-
|
|
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
|
-
*
|
|
5593
|
+
* Safe executable resolution across POSIX and Windows layouts.
|
|
4669
5594
|
*/
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4850
|
-
const
|
|
4851
|
-
const
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
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
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
4891
|
-
const
|
|
4892
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4899
|
-
const
|
|
4900
|
-
const
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
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
|
-
|
|
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 =
|
|
5980
|
+
const homeDir = os9.homedir();
|
|
5022
5981
|
const dirCandidates = [
|
|
5023
|
-
|
|
5024
|
-
|
|
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
|
|
5028
|
-
const entries = await
|
|
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:
|
|
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 =
|
|
5061
|
-
if (await
|
|
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 =
|
|
5065
|
-
if (await
|
|
6023
|
+
const pkgJsonPath = path21.join(cwd, "package.json");
|
|
6024
|
+
if (await fs19.pathExists(pkgJsonPath)) {
|
|
5066
6025
|
try {
|
|
5067
|
-
const pkg = await
|
|
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
|
-
|
|
5091
|
-
|
|
6049
|
+
path21.join(cwd, ".env.local"),
|
|
6050
|
+
path21.join(cwd, ".env")
|
|
5092
6051
|
];
|
|
5093
6052
|
for (const envFile of envFiles) {
|
|
5094
|
-
if (await
|
|
6053
|
+
if (await fs19.pathExists(envFile)) {
|
|
5095
6054
|
try {
|
|
5096
|
-
const content = await
|
|
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:
|
|
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
|
|
6268
|
+
await fs19.ensureDir(path21.dirname(targetFilePath));
|
|
5310
6269
|
let existingConfig = {};
|
|
5311
|
-
if (await
|
|
6270
|
+
if (await fs19.pathExists(targetFilePath)) {
|
|
5312
6271
|
try {
|
|
5313
|
-
existingConfig = await
|
|
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
|
|
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) =>
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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")
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
7324
|
-
const pluginDir = options.plugin ?
|
|
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
|
|
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");
|