agents-united 0.11.0 → 0.13.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 +4 -3
- package/dist/cli.d.ts +26 -1
- package/dist/cli.js +2667 -437
- package/dist/cli.js.map +1 -1
- package/package.json +3 -3
- package/registry/agents/orchestrator-business.md +31 -13
- package/registry/agents/orchestrator-design.md +31 -9
- package/registry/agents/orchestrator-digital-agency.md +24 -27
- package/registry/agents/orchestrator-engineering.md +87 -32
- package/registry/agents/orchestrator-marketing.md +31 -10
- package/registry/agents/orchestrator-research.md +31 -14
- package/registry/agents/orchestrator-security.md +33 -15
- package/registry/agents/orchestrator-system-architecture.md +33 -10
- package/registry/agents/orchestrator-universal.md +34 -0
- package/registry/agents/subagent-accessibility-lead.md +9 -0
- package/registry/agents/subagent-ai-model-architect.md +8 -0
- package/registry/agents/subagent-android-architect.md +9 -0
- package/registry/agents/subagent-appsec-penetration-tester.md +8 -0
- package/registry/agents/subagent-backend-architect.md +29 -0
- package/registry/agents/subagent-business-panel-experts.md +9 -0
- package/registry/agents/subagent-cloud-infrastructure-architect.md +8 -0
- package/registry/agents/subagent-cloud-security-architect.md +8 -0
- package/registry/agents/subagent-code-reviewer.md +24 -11
- package/registry/agents/subagent-compliance-grc-specialist.md +16 -0
- package/registry/agents/subagent-cross-platform-specialist.md +9 -0
- package/registry/agents/subagent-data-engineer.md +9 -0
- package/registry/agents/subagent-database-administrator.md +8 -0
- package/registry/agents/subagent-deep-research.md +8 -0
- package/registry/agents/subagent-design-ops-lead.md +9 -0
- package/registry/agents/subagent-design-researcher.md +9 -0
- package/registry/agents/subagent-design-systems-architect.md +9 -0
- package/registry/agents/subagent-designer-toolkit-expert.md +8 -0
- package/registry/agents/subagent-devops-engineer.md +8 -0
- package/registry/agents/subagent-distributed-systems-architect.md +9 -0
- package/registry/agents/subagent-e2e-tester.md +8 -0
- package/registry/agents/subagent-financial-analyst.md +9 -0
- package/registry/agents/subagent-finops-cost-engineer.md +8 -0
- package/registry/agents/subagent-frontend-architect.md +31 -7
- package/registry/agents/subagent-interaction-designer.md +9 -0
- package/registry/agents/subagent-ios-architect.md +9 -0
- package/registry/agents/subagent-legal-contract-analyst.md +9 -0
- package/registry/agents/subagent-lifecycle-email-specialist.md +9 -0
- package/registry/agents/subagent-literature-patent-analyst.md +9 -0
- package/registry/agents/subagent-market-intelligence-analyst.md +9 -0
- package/registry/agents/subagent-marketing-campaign-specialist.md +16 -0
- package/registry/agents/subagent-marketing-content-strategist.md +16 -0
- package/registry/agents/subagent-marketing-conversion-specialist.md +16 -0
- package/registry/agents/subagent-marketing-creative-designer.md +16 -0
- package/registry/agents/subagent-marketing-growth-strategist.md +16 -0
- package/registry/agents/subagent-ml-platform-engineer.md +8 -0
- package/registry/agents/subagent-operations-strategist.md +9 -0
- package/registry/agents/subagent-paid-acquisition-specialist.md +9 -0
- package/registry/agents/subagent-plg-strategist.md +9 -0
- package/registry/agents/subagent-prototype-tester.md +8 -0
- package/registry/agents/subagent-qa-automation-lead.md +15 -0
- package/registry/agents/subagent-repo-index.md +18 -1
- package/registry/agents/subagent-security-engineer.md +9 -0
- package/registry/agents/subagent-seo-specialist.md +15 -0
- package/registry/agents/subagent-socratic-mentor.md +9 -0
- package/registry/agents/subagent-statistical-analyst.md +8 -0
- package/registry/agents/subagent-sysops-sre-lead.md +8 -0
- package/registry/agents/subagent-system-architect.md +9 -0
- package/registry/agents/subagent-ui-designer.md +9 -0
- package/registry/agents/subagent-ux-strategist.md +9 -0
- package/registry/bundles.json +468 -275
- package/registry/core/orchestrator-engineering.core.md +33 -0
- package/registry/core/subagent-backend-architect.core.md +73 -0
- package/registry/core/subagent-code-reviewer.core.md +83 -0
- package/registry/core/subagent-frontend-architect.core.md +55 -0
- package/registry/core/subagent-repo-index.core.md +73 -0
- package/registry/profiles/claude@2.1.271.json +48 -0
- package/registry/realizations/claude/orchestrator-engineering.json +14 -0
- package/registry/realizations/claude/subagent-backend-architect.json +16 -0
- package/registry/realizations/claude/subagent-code-reviewer.json +15 -0
- package/registry/realizations/claude/subagent-frontend-architect.json +15 -0
- package/registry/realizations/claude/subagent-repo-index.json +15 -0
- package/registry/skills/ab-test-setup/SKILL.md +1 -1
- package/registry/skills/accessibility-audit/SKILL.md +1 -1
- package/registry/skills/clickable-prototype-spec/SKILL.md +1 -1
- package/registry/skills/component-library-management/SKILL.md +1 -1
- package/registry/skills/component-playground-setup/SKILL.md +1 -1
- package/registry/skills/content-calendar-strategy/SKILL.md +1 -1
- package/registry/skills/conversion-funnel-optimization/SKILL.md +1 -1
- package/registry/skills/copywriting-frameworks/SKILL.md +1 -1
- package/registry/skills/design-handoff-spec/SKILL.md +1 -1
- package/registry/skills/design-ops-workflow/SKILL.md +1 -1
- package/registry/skills/design-system-governance/SKILL.md +1 -1
- package/registry/skills/design-system-tokens/SKILL.md +1 -1
- package/registry/skills/design-tokens-management/SKILL.md +1 -1
- package/registry/skills/design-version-control/SKILL.md +1 -1
- package/registry/skills/email-marketing-automation/SKILL.md +1 -1
- package/registry/skills/finishing-a-development-branch/SKILL.md +1 -1
- package/registry/skills/growth-experiment-design/SKILL.md +1 -1
- package/registry/skills/interaction-pattern-library/SKILL.md +1 -1
- package/registry/skills/interactive-prototype-builder/SKILL.md +1 -1
- package/registry/skills/micro-interaction-design/SKILL.md +1 -1
- package/registry/skills/mobile-first-design/SKILL.md +1 -1
- package/registry/skills/product-launch-playbook/SKILL.md +1 -1
- package/registry/skills/responsive-design-audit/SKILL.md +1 -1
- package/registry/skills/seo-audit/SKILL.md +1 -1
- package/registry/skills/signup-flow-cro/SKILL.md +1 -1
- package/registry/skills/social-media-campaign/SKILL.md +1 -1
- package/registry/skills/state-driven-ui-animation/SKILL.md +1 -1
- package/registry/skills/subagent-driven-development/SKILL.md +1 -1
- package/registry/skills/ui-component-spec/SKILL.md +1 -1
- package/registry/skills/usability-testing-protocol/SKILL.md +1 -1
- package/registry/skills/user-flow-mapping/SKILL.md +1 -1
- package/registry/skills/user-journey-mapping/SKILL.md +1 -1
- package/registry/{workflows/workflow-agency-ad-creative-sprint.md → skills/workflow-agency-ad-creative-sprint/SKILL.md} +14 -7
- package/registry/{workflows/workflow-agency-brand-design-system.md → skills/workflow-agency-brand-design-system/SKILL.md} +14 -7
- package/registry/{workflows/workflow-agency-client-pitch-proposal.md → skills/workflow-agency-client-pitch-proposal/SKILL.md} +14 -7
- package/registry/{workflows/workflow-agency-cro-funnel-teardown.md → skills/workflow-agency-cro-funnel-teardown/SKILL.md} +14 -7
- package/registry/{workflows/workflow-agency-full-campaign.md → skills/workflow-agency-full-campaign/SKILL.md} +14 -7
- package/registry/{workflows/workflow-agency-seo-content-engine.md → skills/workflow-agency-seo-content-engine/SKILL.md} +14 -7
- package/registry/{workflows/workflow-analyze.md → skills/workflow-analyze/SKILL.md} +13 -6
- package/registry/{workflows/workflow-api-contract-design.md → skills/workflow-api-contract-design/SKILL.md} +11 -4
- package/registry/{workflows/workflow-app-store-release.md → skills/workflow-app-store-release/SKILL.md} +11 -4
- package/registry/{workflows/workflow-brainstorm.md → skills/workflow-brainstorm/SKILL.md} +14 -7
- package/registry/{workflows/workflow-build.md → skills/workflow-build/SKILL.md} +14 -7
- package/registry/{workflows/workflow-business-panel.md → skills/workflow-business-panel/SKILL.md} +14 -7
- package/registry/{workflows/workflow-cleanup.md → skills/workflow-cleanup/SKILL.md} +14 -8
- package/registry/{workflows/workflow-deploy-staging.md → skills/workflow-deploy-staging/SKILL.md} +11 -4
- package/registry/{workflows/workflow-design-code.md → skills/workflow-design-code/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-ops--handoff.md → skills/workflow-design-ops-handoff/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-ops--plan-sprint.md → skills/workflow-design-ops-plan-sprint/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-ops--setup-workflow.md → skills/workflow-design-ops-setup-workflow/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-orchestrate.md → skills/workflow-design-orchestrate/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-systems--audit-system.md → skills/workflow-design-systems-audit-system/SKILL.md} +14 -7
- package/registry/{workflows/workflow-design-systems--create-component.md → skills/workflow-design-systems-create-component/SKILL.md} +13 -7
- package/registry/{workflows/workflow-design-systems--tokenize.md → skills/workflow-design-systems-tokenize/SKILL.md} +14 -7
- package/registry/{workflows/workflow-diagnose.md → skills/workflow-diagnose/SKILL.md} +11 -4
- package/registry/{workflows/workflow-e2e-testing.md → skills/workflow-e2e-testing/SKILL.md} +11 -4
- package/registry/{workflows/workflow-email-drip-sequence.md → skills/workflow-email-drip-sequence/SKILL.md} +14 -7
- package/registry/{workflows/workflow-estimate.md → skills/workflow-estimate/SKILL.md} +14 -7
- package/registry/{workflows/workflow-explain.md → skills/workflow-explain/SKILL.md} +14 -7
- package/registry/{workflows/workflow-frontend-audit.md → skills/workflow-frontend-audit/SKILL.md} +11 -4
- package/registry/{workflows/workflow-git.md → skills/workflow-git/SKILL.md} +12 -5
- package/registry/{workflows/workflow-grill.md → skills/workflow-grill/SKILL.md} +11 -4
- package/registry/{workflows/workflow-implement.md → skills/workflow-implement/SKILL.md} +15 -8
- package/registry/{workflows/workflow-incident-triage.md → skills/workflow-incident-triage/SKILL.md} +11 -4
- package/registry/{workflows/workflow-interaction-design--design-interaction.md → skills/workflow-interaction-design-design-interaction/SKILL.md} +14 -7
- package/registry/{workflows/workflow-interaction-design--error-flow.md → skills/workflow-interaction-design-error-flow/SKILL.md} +14 -7
- package/registry/{workflows/workflow-interaction-design--map-states.md → skills/workflow-interaction-design-map-states/SKILL.md} +14 -7
- package/registry/{workflows/workflow-marketing-audit.md → skills/workflow-marketing-audit/SKILL.md} +14 -7
- package/registry/{workflows/workflow-marketing-campaign-builder.md → skills/workflow-marketing-campaign-builder/SKILL.md} +14 -7
- package/registry/{workflows/workflow-marketing-content-pipeline.md → skills/workflow-marketing-content-pipeline/SKILL.md} +13 -7
- package/registry/{workflows/workflow-marketing-growth-experiment.md → skills/workflow-marketing-growth-experiment/SKILL.md} +14 -7
- package/registry/{workflows/workflow-marketing-launch.md → skills/workflow-marketing-launch/SKILL.md} +14 -7
- package/registry/{workflows/workflow-marketing-panel.md → skills/workflow-marketing-panel/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ml-eval.md → skills/workflow-ml-eval/SKILL.md} +14 -7
- package/registry/{workflows/workflow-mobile-build.md → skills/workflow-mobile-build/SKILL.md} +13 -6
- package/registry/{workflows/workflow-onboarding-funnel-cro.md → skills/workflow-onboarding-funnel-cro/SKILL.md} +14 -7
- package/registry/{workflows/workflow-paid-acquisition-campaign.md → skills/workflow-paid-acquisition-campaign/SKILL.md} +14 -7
- package/registry/{workflows/workflow-paid-campaign-launch.md → skills/workflow-paid-campaign-launch/SKILL.md} +14 -7
- package/registry/{workflows/workflow-plan.md → skills/workflow-plan/SKILL.md} +14 -7
- package/registry/{workflows/workflow-prototyping-testing--evaluate.md → skills/workflow-prototyping-testing-evaluate/SKILL.md} +14 -7
- package/registry/{workflows/workflow-prototyping-testing--experiment.md → skills/workflow-prototyping-testing-experiment/SKILL.md} +14 -7
- package/registry/{workflows/workflow-prototyping-testing--prototype-plan.md → skills/workflow-prototyping-testing-prototype-plan/SKILL.md} +14 -7
- package/registry/{workflows/workflow-prototyping-testing--test-plan.md → skills/workflow-prototyping-testing-test-plan/SKILL.md} +14 -7
- package/registry/{workflows/workflow-rag-pipeline-deploy.md → skills/workflow-rag-pipeline-deploy/SKILL.md} +14 -7
- package/registry/{workflows/workflow-recommend.md → skills/workflow-recommend/SKILL.md} +14 -7
- package/registry/{workflows/workflow-research.md → skills/workflow-research/SKILL.md} +14 -7
- package/registry/{workflows/workflow-review.md → skills/workflow-review/SKILL.md} +13 -6
- package/registry/{workflows/workflow-seo-audit-pipeline.md → skills/workflow-seo-audit-pipeline/SKILL.md} +14 -7
- package/registry/{workflows/workflow-seo-content-pipeline.md → skills/workflow-seo-content-pipeline/SKILL.md} +14 -7
- package/registry/{workflows/workflow-serverless-gpu-deploy.md → skills/workflow-serverless-gpu-deploy/SKILL.md} +14 -7
- package/registry/{workflows/workflow-spec.md → skills/workflow-spec/SKILL.md} +11 -4
- package/registry/{workflows/workflow-spec-panel.md → skills/workflow-spec-panel/SKILL.md} +14 -7
- package/registry/{workflows/workflow-test.md → skills/workflow-test/SKILL.md} +15 -8
- package/registry/{workflows/workflow-troubleshoot.md → skills/workflow-troubleshoot/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ui-design--color-palette.md → skills/workflow-ui-design-color-palette/SKILL.md} +13 -7
- package/registry/{workflows/workflow-ui-design--design-screen.md → skills/workflow-ui-design-design-screen/SKILL.md} +13 -7
- package/registry/{workflows/workflow-ui-design--responsive-audit.md → skills/workflow-ui-design-responsive-audit/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ui-design--type-system.md → skills/workflow-ui-design-type-system/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ux-strategy--benchmark.md → skills/workflow-ux-strategy-benchmark/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ux-strategy--frame-problem.md → skills/workflow-ux-strategy-frame-problem/SKILL.md} +14 -7
- package/registry/{workflows/workflow-ux-strategy--strategize.md → skills/workflow-ux-strategy-strategize/SKILL.md} +14 -7
- package/registry/translation-ledger.json +26 -0
package/dist/cli.js
CHANGED
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { cac } from "cac";
|
|
5
5
|
import pc from "picocolors";
|
|
6
|
-
import { intro, outro, spinner, note, select, multiselect, confirm, text } from "@clack/prompts";
|
|
7
|
-
import
|
|
8
|
-
import
|
|
6
|
+
import { intro, outro, spinner, note, log, select, multiselect, confirm, text } from "@clack/prompts";
|
|
7
|
+
import fs15 from "fs-extra";
|
|
8
|
+
import path17 from "path";
|
|
9
9
|
|
|
10
10
|
// src/core/registry.ts
|
|
11
11
|
import path from "path";
|
|
12
12
|
import fs from "fs-extra";
|
|
13
|
+
import yaml from "yaml";
|
|
13
14
|
import { fileURLToPath } from "url";
|
|
14
15
|
var __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
var __dirname = path.dirname(__filename);
|
|
16
|
-
var RegistryResolver = class {
|
|
17
|
+
var RegistryResolver = class _RegistryResolver {
|
|
18
|
+
/**
|
|
19
|
+
* Host entrypoint rules (Plan 015 §0/C7). `GEMINI.md` is always resolved so
|
|
20
|
+
* Antigravity-family hosts keep their entrypoint; it is never declared in an
|
|
21
|
+
* agent's frontmatter.
|
|
22
|
+
*/
|
|
23
|
+
static BASELINE_RULES = ["GEMINI.md"];
|
|
17
24
|
registryDir;
|
|
18
25
|
bundlesManifest = null;
|
|
26
|
+
/** Memoized agent file -> declared rules (Plan 015 §0/C6). */
|
|
27
|
+
agentRuleCache = /* @__PURE__ */ new Map();
|
|
19
28
|
constructor(customRegistryDir) {
|
|
20
29
|
if (customRegistryDir) {
|
|
21
30
|
this.registryDir = path.resolve(customRegistryDir);
|
|
@@ -71,18 +80,20 @@ var RegistryResolver = class {
|
|
|
71
80
|
const agents = /* @__PURE__ */ new Set();
|
|
72
81
|
const skills = /* @__PURE__ */ new Set();
|
|
73
82
|
const workflows = /* @__PURE__ */ new Set();
|
|
83
|
+
const rules = new Set(_RegistryResolver.BASELINE_RULES);
|
|
74
84
|
for (const b of domainBundles) {
|
|
75
85
|
const resolved = await this.resolve(b.name);
|
|
76
86
|
resolved.agents.forEach((a) => agents.add(a));
|
|
77
87
|
resolved.skills.forEach((s) => skills.add(s));
|
|
78
88
|
resolved.workflows.forEach((w) => workflows.add(w));
|
|
89
|
+
resolved.rules.forEach((r) => rules.add(r));
|
|
79
90
|
}
|
|
80
91
|
return {
|
|
81
92
|
targetBundle: `domain:${domainName}`,
|
|
82
93
|
agents: Array.from(agents),
|
|
83
94
|
skills: Array.from(skills),
|
|
84
95
|
workflows: Array.from(workflows),
|
|
85
|
-
rules:
|
|
96
|
+
rules: Array.from(rules).sort()
|
|
86
97
|
};
|
|
87
98
|
}
|
|
88
99
|
}
|
|
@@ -91,7 +102,8 @@ var RegistryResolver = class {
|
|
|
91
102
|
const agents = /* @__PURE__ */ new Set();
|
|
92
103
|
const skills = new Set(bundle.skills || []);
|
|
93
104
|
const workflows = new Set(bundle.workflows || []);
|
|
94
|
-
const rules =
|
|
105
|
+
const rules = new Set(_RegistryResolver.BASELINE_RULES);
|
|
106
|
+
(bundle.rules || []).forEach((r) => rules.add(r));
|
|
95
107
|
if (bundle.parentBundle) {
|
|
96
108
|
const parent = await this.getBundle(bundle.parentBundle);
|
|
97
109
|
if (parent) {
|
|
@@ -99,16 +111,24 @@ var RegistryResolver = class {
|
|
|
99
111
|
if (parent.agents) parent.agents.forEach((a) => agents.add(a));
|
|
100
112
|
if (parent.skills) parent.skills.forEach((s) => skills.add(s));
|
|
101
113
|
if (parent.workflows) parent.workflows.forEach((w) => workflows.add(w));
|
|
114
|
+
(parent.rules || []).forEach((r) => rules.add(r));
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
117
|
if (bundle.orchestrator) agents.add(bundle.orchestrator);
|
|
105
118
|
if (bundle.agents) bundle.agents.forEach((a) => agents.add(a));
|
|
119
|
+
for (const agentFile of agents) {
|
|
120
|
+
for (const rule of await this.extractRulesFromAgent(agentFile)) {
|
|
121
|
+
rules.add(rule);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const sortedRules = Array.from(rules).sort();
|
|
125
|
+
this.assertRulesExist(sortedRules, `bundle "${bundle.name}"`);
|
|
106
126
|
return {
|
|
107
127
|
targetBundle: bundle.name,
|
|
108
128
|
agents: Array.from(agents),
|
|
109
129
|
skills: Array.from(skills),
|
|
110
130
|
workflows: Array.from(workflows),
|
|
111
|
-
rules
|
|
131
|
+
rules: sortedRules
|
|
112
132
|
};
|
|
113
133
|
}
|
|
114
134
|
const agentPath = path.join(this.registryDir, "agents", identifier.endsWith(".md") ? identifier : `${identifier}.md`);
|
|
@@ -120,7 +140,9 @@ var RegistryResolver = class {
|
|
|
120
140
|
rules: []
|
|
121
141
|
};
|
|
122
142
|
}
|
|
143
|
+
const sanitizedSkillId = identifier.replace(/\.md$/, "").replace(/--/g, "-");
|
|
123
144
|
const skillPath = path.join(this.registryDir, "skills", identifier);
|
|
145
|
+
const sanitizedSkillPath = path.join(this.registryDir, "skills", sanitizedSkillId);
|
|
124
146
|
if (await fs.pathExists(skillPath)) {
|
|
125
147
|
return {
|
|
126
148
|
agents: [],
|
|
@@ -128,6 +150,22 @@ var RegistryResolver = class {
|
|
|
128
150
|
workflows: [],
|
|
129
151
|
rules: []
|
|
130
152
|
};
|
|
153
|
+
} else if (await fs.pathExists(sanitizedSkillPath)) {
|
|
154
|
+
return {
|
|
155
|
+
agents: [],
|
|
156
|
+
skills: [sanitizedSkillId],
|
|
157
|
+
workflows: [],
|
|
158
|
+
rules: []
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const workflowSkillPath = path.join(this.registryDir, "skills", `workflow-${sanitizedSkillId}`);
|
|
162
|
+
if (await fs.pathExists(workflowSkillPath)) {
|
|
163
|
+
return {
|
|
164
|
+
agents: [],
|
|
165
|
+
skills: [`workflow-${sanitizedSkillId}`],
|
|
166
|
+
workflows: [],
|
|
167
|
+
rules: []
|
|
168
|
+
};
|
|
131
169
|
}
|
|
132
170
|
const workflowFileName = identifier.startsWith("workflow-") ? `${identifier}.md` : `workflow-${identifier}.md`;
|
|
133
171
|
const workflowPath = path.join(this.registryDir, "workflows", workflowFileName);
|
|
@@ -167,7 +205,7 @@ var RegistryResolver = class {
|
|
|
167
205
|
}
|
|
168
206
|
const skillsDir = path.join(this.registryDir, "skills");
|
|
169
207
|
let matchedSkills = [];
|
|
170
|
-
if (!options?.type || options.type === "skill") {
|
|
208
|
+
if (!options?.type || options.type === "skill" || options.type === "workflow") {
|
|
171
209
|
if (await fs.pathExists(skillsDir)) {
|
|
172
210
|
const dirs = await fs.readdir(skillsDir);
|
|
173
211
|
matchedSkills = dirs.filter((d) => {
|
|
@@ -176,15 +214,19 @@ var RegistryResolver = class {
|
|
|
176
214
|
});
|
|
177
215
|
}
|
|
178
216
|
}
|
|
179
|
-
const workflowsDir = path.join(this.registryDir, "workflows");
|
|
180
217
|
let matchedWorkflows = [];
|
|
181
218
|
if (!options?.type || options.type === "workflow") {
|
|
219
|
+
matchedWorkflows = matchedSkills.filter((s) => s.startsWith("workflow-"));
|
|
220
|
+
const workflowsDir = path.join(this.registryDir, "workflows");
|
|
182
221
|
if (await fs.pathExists(workflowsDir)) {
|
|
183
222
|
const files = await fs.readdir(workflowsDir);
|
|
184
|
-
|
|
223
|
+
const legacyMatches = files.filter((f) => f.endsWith(".md")).filter((f) => {
|
|
185
224
|
if (!q) return true;
|
|
186
225
|
return f.toLowerCase().includes(q);
|
|
187
226
|
});
|
|
227
|
+
legacyMatches.forEach((f) => {
|
|
228
|
+
if (!matchedWorkflows.includes(f)) matchedWorkflows.push(f);
|
|
229
|
+
});
|
|
188
230
|
}
|
|
189
231
|
}
|
|
190
232
|
return {
|
|
@@ -194,6 +236,55 @@ var RegistryResolver = class {
|
|
|
194
236
|
workflows: matchedWorkflows
|
|
195
237
|
};
|
|
196
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* Plan 015 Step 1 / §0/C6 — extract the `rules:` binding from an agent's YAML
|
|
241
|
+
* frontmatter. Rules are declared as a BLOCK SEQUENCE:
|
|
242
|
+
* rules:
|
|
243
|
+
* - git-guardrails.md
|
|
244
|
+
* Never regex for an inline `rules: [a, b]`. Returns `[]` (never throws) when
|
|
245
|
+
* the file is missing, has no frontmatter, or the YAML is unparsable so that
|
|
246
|
+
* resolution stays resilient; existence of the referenced rule files is
|
|
247
|
+
* enforced separately by `assertRulesExist`.
|
|
248
|
+
*/
|
|
249
|
+
async extractRulesFromAgent(agentFileName) {
|
|
250
|
+
const cached = this.agentRuleCache.get(agentFileName);
|
|
251
|
+
if (cached) {
|
|
252
|
+
return cached;
|
|
253
|
+
}
|
|
254
|
+
let declared = [];
|
|
255
|
+
const agentPath = path.join(this.registryDir, "agents", agentFileName);
|
|
256
|
+
if (await fs.pathExists(agentPath)) {
|
|
257
|
+
const content = await fs.readFile(agentPath, "utf8");
|
|
258
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
259
|
+
if (match) {
|
|
260
|
+
try {
|
|
261
|
+
const meta = yaml.parse(match[1]) ?? {};
|
|
262
|
+
if (Array.isArray(meta.rules)) {
|
|
263
|
+
declared = meta.rules.filter((r) => typeof r === "string");
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
declared = [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.agentRuleCache.set(agentFileName, declared);
|
|
271
|
+
return declared;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Plan 015 §0/D2 — a declared rule binding that cannot be satisfied is a
|
|
275
|
+
* configuration defect, not something to skip silently: installer.ts guards
|
|
276
|
+
* its copy loop with `pathExists`, so a typo would otherwise deploy no rule at
|
|
277
|
+
* all with no error surfaced anywhere.
|
|
278
|
+
*/
|
|
279
|
+
assertRulesExist(rules, context) {
|
|
280
|
+
for (const rule of rules) {
|
|
281
|
+
if (!fs.existsSync(path.join(this.registryDir, "rules", rule))) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Registry validation error: ${context} references rule "${rule}" which does not exist in registry/rules/.`
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
197
288
|
/**
|
|
198
289
|
* ADR 0015 — fail-fast validation of planningLoop configuration.
|
|
199
290
|
* @throws if any bundle violates planner-orchestrator invariants.
|
|
@@ -201,6 +292,15 @@ var RegistryResolver = class {
|
|
|
201
292
|
validateBundles(manifest) {
|
|
202
293
|
const validModes = ["subagent-first", "planner-orchestrator"];
|
|
203
294
|
for (const [name, bundle] of Object.entries(manifest.bundles)) {
|
|
295
|
+
if (Array.isArray(bundle.rules)) {
|
|
296
|
+
for (const rule of bundle.rules) {
|
|
297
|
+
if (!fs.existsSync(path.join(this.registryDir, "rules", rule))) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Registry validation error: bundle "${name}" references rule "${rule}" which does not exist in registry/rules/.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
204
304
|
const pl = bundle.planningLoop;
|
|
205
305
|
if (!pl || !pl.enabled) continue;
|
|
206
306
|
const mode = pl.mode ?? "subagent-first";
|
|
@@ -209,6 +309,19 @@ var RegistryResolver = class {
|
|
|
209
309
|
`Registry validation error: bundle "${name}" has unknown planningLoop.mode "${mode}". Valid modes: ${validModes.join(", ")}`
|
|
210
310
|
);
|
|
211
311
|
}
|
|
312
|
+
if (pl.budget) {
|
|
313
|
+
if (bundle.tier !== "organization") {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Registry validation error: bundle "${name}" declares a Consultation Budget but is not an organization-tier bundle. Iteration caps (planningLoop.budget.maxIterations) are Tier-2-only.`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const cap = pl.budget.maxIterations;
|
|
319
|
+
if (!Number.isInteger(cap) || cap < 1) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`Registry validation error: bundle "${name}" has planningLoop.budget.maxIterations ${String(cap)}; expected a positive integer.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
212
325
|
if (mode === "planner-orchestrator") {
|
|
213
326
|
if (pl.budget) {
|
|
214
327
|
throw new Error(
|
|
@@ -226,9 +339,9 @@ var RegistryResolver = class {
|
|
|
226
339
|
};
|
|
227
340
|
|
|
228
341
|
// src/core/installer.ts
|
|
229
|
-
import
|
|
342
|
+
import path6 from "path";
|
|
230
343
|
import crypto from "crypto";
|
|
231
|
-
import
|
|
344
|
+
import fs4 from "fs-extra";
|
|
232
345
|
|
|
233
346
|
// src/core/adapter.ts
|
|
234
347
|
import path3 from "path";
|
|
@@ -245,7 +358,8 @@ var HOST_REGISTRY = {
|
|
|
245
358
|
agentsSubdir: "agents",
|
|
246
359
|
detectionMarkers: [".agents"],
|
|
247
360
|
profile: "antigravity",
|
|
248
|
-
projectionCapable: false
|
|
361
|
+
projectionCapable: false,
|
|
362
|
+
status: "supported"
|
|
249
363
|
},
|
|
250
364
|
gemini: {
|
|
251
365
|
id: "gemini",
|
|
@@ -255,7 +369,8 @@ var HOST_REGISTRY = {
|
|
|
255
369
|
agentsSubdir: "agents",
|
|
256
370
|
detectionMarkers: [".gemini"],
|
|
257
371
|
profile: "antigravity",
|
|
258
|
-
projectionCapable: false
|
|
372
|
+
projectionCapable: false,
|
|
373
|
+
status: "supported"
|
|
259
374
|
},
|
|
260
375
|
claude: {
|
|
261
376
|
id: "claude",
|
|
@@ -265,7 +380,8 @@ var HOST_REGISTRY = {
|
|
|
265
380
|
agentsSubdir: "agents",
|
|
266
381
|
detectionMarkers: [".claude"],
|
|
267
382
|
profile: "claude-code",
|
|
268
|
-
projectionCapable: true
|
|
383
|
+
projectionCapable: true,
|
|
384
|
+
status: "supported"
|
|
269
385
|
},
|
|
270
386
|
cursor: {
|
|
271
387
|
id: "cursor",
|
|
@@ -275,7 +391,8 @@ var HOST_REGISTRY = {
|
|
|
275
391
|
agentsSubdir: "agents",
|
|
276
392
|
detectionMarkers: [".cursor"],
|
|
277
393
|
profile: "cursor",
|
|
278
|
-
projectionCapable: true
|
|
394
|
+
projectionCapable: true,
|
|
395
|
+
status: "under-development"
|
|
279
396
|
},
|
|
280
397
|
cline: {
|
|
281
398
|
id: "cline",
|
|
@@ -285,7 +402,8 @@ var HOST_REGISTRY = {
|
|
|
285
402
|
agentsSubdir: "agents",
|
|
286
403
|
detectionMarkers: [".cline", ".clinerules"],
|
|
287
404
|
profile: "cline",
|
|
288
|
-
projectionCapable: true
|
|
405
|
+
projectionCapable: true,
|
|
406
|
+
status: "supported"
|
|
289
407
|
},
|
|
290
408
|
opencode: {
|
|
291
409
|
id: "opencode",
|
|
@@ -295,7 +413,8 @@ var HOST_REGISTRY = {
|
|
|
295
413
|
agentsSubdir: "agent",
|
|
296
414
|
detectionMarkers: [".opencode", "opencode.json"],
|
|
297
415
|
profile: "opencode",
|
|
298
|
-
projectionCapable: true
|
|
416
|
+
projectionCapable: true,
|
|
417
|
+
status: "under-development"
|
|
299
418
|
},
|
|
300
419
|
codex: {
|
|
301
420
|
id: "codex",
|
|
@@ -305,13 +424,29 @@ var HOST_REGISTRY = {
|
|
|
305
424
|
agentsSubdir: void 0,
|
|
306
425
|
detectionMarkers: ["AGENTS.md", ".codex"],
|
|
307
426
|
profile: "agentsmd",
|
|
308
|
-
projectionCapable: true
|
|
427
|
+
projectionCapable: true,
|
|
428
|
+
status: "under-development"
|
|
309
429
|
}
|
|
310
430
|
};
|
|
311
431
|
var KNOWN_HOST_IDS = Object.keys(HOST_REGISTRY);
|
|
312
432
|
function isKnownHost(id) {
|
|
313
433
|
return Object.prototype.hasOwnProperty.call(HOST_REGISTRY, id);
|
|
314
434
|
}
|
|
435
|
+
var SUPPORTED_HOST_IDS = Object.values(HOST_REGISTRY).filter((h) => h.status === "supported").map((h) => h.id);
|
|
436
|
+
var UNDER_DEVELOPMENT_HOST_IDS = Object.values(HOST_REGISTRY).filter((h) => h.status === "under-development").map((h) => h.id);
|
|
437
|
+
var PLANNED_HOSTS = [
|
|
438
|
+
{ id: "kimi", label: "Kimi / Moonshot" }
|
|
439
|
+
];
|
|
440
|
+
function hostAvailabilityNotice() {
|
|
441
|
+
const supported = SUPPORTED_HOST_IDS.map((id) => HOST_REGISTRY[id].label).join(" \xB7 ");
|
|
442
|
+
const underDev = UNDER_DEVELOPMENT_HOST_IDS.map((id) => HOST_REGISTRY[id].label).join(" \xB7 ");
|
|
443
|
+
const planned = PLANNED_HOSTS.map((h) => h.label).join(" \xB7 ");
|
|
444
|
+
return [
|
|
445
|
+
`\u2705 Supported: ${supported}`,
|
|
446
|
+
`\u{1F6A7} Under Development (unavailable): ${underDev}`,
|
|
447
|
+
`\u{1F5D3} Planned: ${planned} (projector on the planning list)`
|
|
448
|
+
].join("\n");
|
|
449
|
+
}
|
|
315
450
|
function resolveHostProjectDir(host, cwd) {
|
|
316
451
|
return path2.resolve(cwd, HOST_REGISTRY[host].projectDir);
|
|
317
452
|
}
|
|
@@ -367,10 +502,10 @@ var AgentHostAdapter = class {
|
|
|
367
502
|
import YAML from "yaml";
|
|
368
503
|
|
|
369
504
|
// src/core/cline-projector.ts
|
|
370
|
-
import
|
|
505
|
+
import yaml2 from "yaml";
|
|
371
506
|
import path4 from "path";
|
|
372
507
|
import fs2 from "fs-extra";
|
|
373
|
-
var ClineProjector = class {
|
|
508
|
+
var ClineProjector = class _ClineProjector {
|
|
374
509
|
/**
|
|
375
510
|
* Managed marker inserted after YAML frontmatter in every rendered artifact.
|
|
376
511
|
*/
|
|
@@ -411,7 +546,7 @@ complete the role in the main session.`;
|
|
|
411
546
|
}
|
|
412
547
|
let parsed = {};
|
|
413
548
|
try {
|
|
414
|
-
parsed =
|
|
549
|
+
parsed = yaml2.parse(match[1]) || {};
|
|
415
550
|
} catch {
|
|
416
551
|
parsed = {};
|
|
417
552
|
}
|
|
@@ -428,7 +563,7 @@ complete the role in the main session.`;
|
|
|
428
563
|
} else if (typeof defaultMaxIterations === "number") {
|
|
429
564
|
cleanFrontmatter.maxIterations = defaultMaxIterations;
|
|
430
565
|
}
|
|
431
|
-
const frontmatterStr =
|
|
566
|
+
const frontmatterStr = yaml2.stringify(cleanFrontmatter).trim();
|
|
432
567
|
const bodyStr = match[2].trim();
|
|
433
568
|
return `---
|
|
434
569
|
${frontmatterStr}
|
|
@@ -450,7 +585,7 @@ ${bodyStr}
|
|
|
450
585
|
let humanName;
|
|
451
586
|
if (match) {
|
|
452
587
|
try {
|
|
453
|
-
const parsed =
|
|
588
|
+
const parsed = yaml2.parse(match[1]) || {};
|
|
454
589
|
if (typeof parsed.name === "string" && parsed.name.trim().length > 0) {
|
|
455
590
|
humanName = parsed.name.trim();
|
|
456
591
|
}
|
|
@@ -471,7 +606,7 @@ ${bodyStr}
|
|
|
471
606
|
let description;
|
|
472
607
|
if (match) {
|
|
473
608
|
try {
|
|
474
|
-
const parsed =
|
|
609
|
+
const parsed = yaml2.parse(match[1]) || {};
|
|
475
610
|
if (typeof parsed.description === "string" && parsed.description.trim().length > 0) {
|
|
476
611
|
description = parsed.description.trim();
|
|
477
612
|
}
|
|
@@ -480,7 +615,7 @@ ${bodyStr}
|
|
|
480
615
|
}
|
|
481
616
|
const cleanFrontmatter = { name: slug };
|
|
482
617
|
if (description) cleanFrontmatter.description = description;
|
|
483
|
-
const frontmatterStr =
|
|
618
|
+
const frontmatterStr = yaml2.stringify(cleanFrontmatter).trim();
|
|
484
619
|
const bodyStr = match ? match[2].trim() : canonicalContent.trim();
|
|
485
620
|
return `---
|
|
486
621
|
${frontmatterStr}
|
|
@@ -506,11 +641,11 @@ ${canonicalContent.trim()}`;
|
|
|
506
641
|
const rawBody = match[2];
|
|
507
642
|
let parsed = {};
|
|
508
643
|
try {
|
|
509
|
-
parsed =
|
|
644
|
+
parsed = yaml2.parse(rawYaml) || {};
|
|
510
645
|
} catch {
|
|
511
646
|
parsed = {};
|
|
512
647
|
}
|
|
513
|
-
const frontmatterStr =
|
|
648
|
+
const frontmatterStr = yaml2.stringify(parsed).trim();
|
|
514
649
|
const bodyStr = rawBody.trim();
|
|
515
650
|
return `---
|
|
516
651
|
${frontmatterStr}
|
|
@@ -534,7 +669,8 @@ ${bodyStr}
|
|
|
534
669
|
canonicalPath: `agents/${agentFile}`
|
|
535
670
|
}));
|
|
536
671
|
const sortedSkills = [...bundle.skills || []].sort();
|
|
537
|
-
const
|
|
672
|
+
const rawWorkflows = bundle.workflows && bundle.workflows.length > 0 ? bundle.workflows : (bundle.skills || []).filter((s) => s.startsWith("workflow-"));
|
|
673
|
+
const sortedWorkflows = [...rawWorkflows].sort();
|
|
538
674
|
const recommendedAddons = (bundle.recommendedAddons || []).filter((a) => !excludeAddons.includes(a));
|
|
539
675
|
const manifest = {
|
|
540
676
|
schemaVersion: 1,
|
|
@@ -566,7 +702,7 @@ ${bodyStr}
|
|
|
566
702
|
manifest.personas = Object.entries(bundle.personaAliases).map(([persona, role]) => ({ persona, role }));
|
|
567
703
|
}
|
|
568
704
|
}
|
|
569
|
-
return
|
|
705
|
+
return yaml2.stringify(manifest);
|
|
570
706
|
}
|
|
571
707
|
/**
|
|
572
708
|
* Render the coordinator rule file (.agents/plugins/<bundle>/rules/agents-united-<bundle>.md).
|
|
@@ -581,8 +717,18 @@ ${bodyStr}
|
|
|
581
717
|
const coordinatorCanonical = `agents/${coordinatorFile}`;
|
|
582
718
|
const specialistLines = (bundle.agents || []).map((agentFile) => {
|
|
583
719
|
const name = agentFile.replace(/\.md$/, "");
|
|
584
|
-
|
|
720
|
+
const projected = this.stripSubagentPrefix(name);
|
|
721
|
+
return `- **${name}**: \`.cline/agents/${projected}.yml\` | \`.agents/agents/${agentFile}\``;
|
|
585
722
|
});
|
|
723
|
+
const rawWorkflows = [
|
|
724
|
+
...(bundle.workflows || []).map((w) => w.replace(/\.md$/i, "")),
|
|
725
|
+
...(bundle.skills || []).filter((s) => s.startsWith("workflow-"))
|
|
726
|
+
];
|
|
727
|
+
const uniqueWorkflows = Array.from(new Set(rawWorkflows)).sort();
|
|
728
|
+
const workflowSection = uniqueWorkflows.length > 0 ? `
|
|
729
|
+
### Installed Workflows & Workflow Skills
|
|
730
|
+
Execute multi-step engineering procedures by consulting the corresponding workflow skill runbook (\`.agents/skills/<name>/SKILL.md\`) or triggering its slash command (\`/<name>\`):
|
|
731
|
+
` + uniqueWorkflows.map((w) => `- **${w}**: \`.cline/workflows/${w}.md\` | \`.agents/skills/${w}/SKILL.md\``).join("\n") + "\n" : "";
|
|
586
732
|
const addons = (bundle.recommendedAddons || []).filter((a) => !excludeAddons.includes(a));
|
|
587
733
|
const addonSection = addons.length > 0 ? `
|
|
588
734
|
### Recommended Addon Policy
|
|
@@ -617,7 +763,7 @@ ${planningSection}${personaSection}
|
|
|
617
763
|
${specialistLines.length > 0 ? `
|
|
618
764
|
### Installed Specialist Roles
|
|
619
765
|
${specialistLines.join("\n")}` : ""}
|
|
620
|
-
${addonSection}
|
|
766
|
+
${workflowSection}${addonSection}
|
|
621
767
|
`;
|
|
622
768
|
}
|
|
623
769
|
/**
|
|
@@ -694,7 +840,24 @@ ${addonSection}
|
|
|
694
840
|
}
|
|
695
841
|
}
|
|
696
842
|
}
|
|
697
|
-
|
|
843
|
+
const workflowSkillNames = resolved.skills.filter((s) => s.startsWith("workflow-"));
|
|
844
|
+
for (const skillName of workflowSkillNames) {
|
|
845
|
+
const canonicalRel = `skills/${skillName}/SKILL.md`;
|
|
846
|
+
const skillFilePath = path4.join(registryDir, "skills", skillName, "SKILL.md");
|
|
847
|
+
if (await fs2.pathExists(skillFilePath)) {
|
|
848
|
+
const content = await fs2.readFile(skillFilePath, "utf8");
|
|
849
|
+
const slug = skillName;
|
|
850
|
+
const rendered = this.renderWorkflowProjection(content, canonicalRel);
|
|
851
|
+
artifacts.push({
|
|
852
|
+
kind: "workflow",
|
|
853
|
+
canonical: canonicalRel,
|
|
854
|
+
relPath: `.cline/workflows/${slug}.md`.replace(/\\/g, "/"),
|
|
855
|
+
content: rendered,
|
|
856
|
+
managedMarker: true
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const workflowFile of resolved.workflows || []) {
|
|
698
861
|
const canonicalRel = `workflows/${workflowFile}`;
|
|
699
862
|
const srcPath = path4.join(registryDir, "workflows", workflowFile);
|
|
700
863
|
if (await fs2.pathExists(srcPath)) {
|
|
@@ -717,6 +880,24 @@ ${addonSection}
|
|
|
717
880
|
content: ruleContent,
|
|
718
881
|
managedMarker: true
|
|
719
882
|
});
|
|
883
|
+
const hostEntrypointRules = /* @__PURE__ */ new Set(["GEMINI.md", "AGENTS.md", "CLAUDE.md", "CURSOR.md"]);
|
|
884
|
+
for (const ruleFile of [...resolved.rules || []].sort()) {
|
|
885
|
+
if (hostEntrypointRules.has(ruleFile)) continue;
|
|
886
|
+
const ruleSrcPath = path4.join(registryDir, "rules", ruleFile);
|
|
887
|
+
if (await fs2.pathExists(ruleSrcPath)) {
|
|
888
|
+
const ruleFileContent = await fs2.readFile(ruleSrcPath, "utf8");
|
|
889
|
+
artifacts.push({
|
|
890
|
+
kind: "rule",
|
|
891
|
+
canonical: `rules/${ruleFile}`,
|
|
892
|
+
relPath: `.cline/rules/${ruleFile}`.replace(/\\/g, "/"),
|
|
893
|
+
content: `${_ClineProjector.marker(`rules/${ruleFile}`)}
|
|
894
|
+
|
|
895
|
+
${ruleFileContent.trim()}
|
|
896
|
+
`,
|
|
897
|
+
managedMarker: true
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
720
901
|
const manifestContent = this.renderTeamManifest(bundle, scope, excludeAddons);
|
|
721
902
|
artifacts.push({
|
|
722
903
|
kind: "team-manifest",
|
|
@@ -920,6 +1101,765 @@ ${normalizedBody}`;
|
|
|
920
1101
|
}
|
|
921
1102
|
};
|
|
922
1103
|
|
|
1104
|
+
// src/core/claude-projector.ts
|
|
1105
|
+
import yaml3 from "yaml";
|
|
1106
|
+
|
|
1107
|
+
// src/core/guard.ts
|
|
1108
|
+
var GUARD_SCRIPT = String.raw`let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{let i={};try{i=JSON.parse(s)}catch(e){}const t=i.tool_input||{},c=String(t.command||""),f=String(t.file_path||"").replace(/\\/g,"/");let r="";if(/\bgit\b[^;&|]*\bpush\b[^;&|]*(--force(?!-with-lease)\b|(^|\s)-f\b)/.test(c))r="git push --force";else if(/\bvercel\b[^;&|]*--prod\b/.test(c))r="vercel --prod";else if(/(^|\/)\.env(\.(?!example$)[^\/]+)?$/.test(f)||/>\s*(\S*\/)?\.env(\.(?!example\b)\S+)?(\s|$)/.test(c))r="a .env write";if(r){process.stderr.write("Blocked by agents-united guard: "+r+" requires explicit human approval outside the agent session.\n");process.exit(2)}})`;
|
|
1109
|
+
function guardHandler() {
|
|
1110
|
+
return { type: "command", command: "node", args: ["-e", GUARD_SCRIPT] };
|
|
1111
|
+
}
|
|
1112
|
+
function managedGuardHooks() {
|
|
1113
|
+
return {
|
|
1114
|
+
PreToolUse: [
|
|
1115
|
+
{ matcher: "Bash", hooks: [guardHandler()] },
|
|
1116
|
+
{ matcher: "Write|Edit|NotebookEdit", hooks: [guardHandler()] }
|
|
1117
|
+
]
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/core/claude-projector.ts
|
|
1122
|
+
import path5 from "path";
|
|
1123
|
+
import fs3 from "fs-extra";
|
|
1124
|
+
|
|
1125
|
+
// src/core/residue-patterns.ts
|
|
1126
|
+
var RESIDUE_PATTERNS_BY_HOST = {
|
|
1127
|
+
claude: [/Nested Subagent Delegation/, /Host Routing/, /language_server/, /Cline & CLI/]
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
// src/core/overlays.ts
|
|
1131
|
+
var OVERLAY_FIELD_VOCAB = {
|
|
1132
|
+
permissionMode: ["default", "acceptEdits", "bypassPermissions", "plan", "delegate"],
|
|
1133
|
+
model: ["opus", "sonnet", "haiku", "inherit"],
|
|
1134
|
+
effort: ["low", "medium", "high", "inherit"]
|
|
1135
|
+
};
|
|
1136
|
+
var CLAUDE_FIELD_POLICY = {
|
|
1137
|
+
name: "keep",
|
|
1138
|
+
description: "keep",
|
|
1139
|
+
model: "map",
|
|
1140
|
+
tools: "map",
|
|
1141
|
+
permissionMode: "map",
|
|
1142
|
+
effort: "map",
|
|
1143
|
+
skills: "map",
|
|
1144
|
+
projections: "map",
|
|
1145
|
+
version: "drop",
|
|
1146
|
+
type: "drop",
|
|
1147
|
+
commandExecutionPolicy: "drop",
|
|
1148
|
+
mainAgent: "drop",
|
|
1149
|
+
subagent: "drop",
|
|
1150
|
+
hooks: "drop",
|
|
1151
|
+
mcpServers: "drop",
|
|
1152
|
+
rules: "drop",
|
|
1153
|
+
inheritCustomizations: "drop"
|
|
1154
|
+
};
|
|
1155
|
+
function validateProjectionOverlays(projections, spec) {
|
|
1156
|
+
if (projections === void 0 || projections === null) return;
|
|
1157
|
+
if (typeof projections !== "object" || Array.isArray(projections)) {
|
|
1158
|
+
throw new Error("Projection overlay validation failed: projections must be an object.");
|
|
1159
|
+
}
|
|
1160
|
+
for (const [host, overlay] of Object.entries(projections)) {
|
|
1161
|
+
if (host !== spec.id) {
|
|
1162
|
+
throw new Error(`Projection overlay validation failed: unknown overlay host "${host}".`);
|
|
1163
|
+
}
|
|
1164
|
+
if (overlay === null || typeof overlay !== "object" || Array.isArray(overlay)) {
|
|
1165
|
+
throw new Error(`Projection overlay validation failed: overlay for "${host}" must be an object.`);
|
|
1166
|
+
}
|
|
1167
|
+
for (const [field, value] of Object.entries(overlay)) {
|
|
1168
|
+
const policy = spec.fields[field];
|
|
1169
|
+
if (policy === void 0) {
|
|
1170
|
+
throw new Error(`Projection overlay validation failed: unknown overlay field "${field}".`);
|
|
1171
|
+
}
|
|
1172
|
+
if (policy === "drop") {
|
|
1173
|
+
throw new Error(
|
|
1174
|
+
`Projection overlay validation failed: "${field}" is unsupported on host "${host}" and must never be introduced by an overlay.`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
const vocab = OVERLAY_FIELD_VOCAB[field];
|
|
1178
|
+
if (vocab && (typeof value !== "string" || !vocab.includes(value))) {
|
|
1179
|
+
throw new Error(
|
|
1180
|
+
`Projection overlay validation failed: invalid value "${String(value)}" for "${field}" (allowed: ${vocab.join(", ")}).`
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// src/core/claude-projector.ts
|
|
1188
|
+
var INLINE_KEYS = /* @__PURE__ */ new Set([
|
|
1189
|
+
"name",
|
|
1190
|
+
"description",
|
|
1191
|
+
"model",
|
|
1192
|
+
"permissionMode",
|
|
1193
|
+
"effort",
|
|
1194
|
+
"tools",
|
|
1195
|
+
"maxIterations",
|
|
1196
|
+
"projections"
|
|
1197
|
+
]);
|
|
1198
|
+
var FEATURE_LEDGER = {
|
|
1199
|
+
version: { disposition: "mapped", rationale: "Authoring metadata for the canonical store; no Claude field needed." },
|
|
1200
|
+
type: { disposition: "mapped", rationale: "Canonical role discriminator; Claude distinguishes roles by file location." },
|
|
1201
|
+
mainAgent: { disposition: "mapped", rationale: "Canonical masthead flag; Claude decides main-thread status at launch (--agent)." },
|
|
1202
|
+
subagent: { disposition: "mapped", rationale: "Canonical masthead flag; every file under .claude/agents/ is a subagent." },
|
|
1203
|
+
inheritCustomizations: { disposition: "mapped", rationale: "Claude loads CLAUDE.md and project memory unless omitClaudeMd is set (flag-gated)." },
|
|
1204
|
+
commandExecutionPolicy: { disposition: "unsupported", rationale: "Antigravity shell policy has no Claude frontmatter equivalent; Bash permissions cover it." },
|
|
1205
|
+
hooks: { disposition: "unsupported", rationale: "Antigravity hook schema (PreInvocation/PostInvocation matchers) differs from Claude hook events; canonical prose hooks are not translated and stay advisory. The one enforced hook is the managed PreToolUse guard every role carries (Plan 022 H5)." },
|
|
1206
|
+
rules: { disposition: "mapped", rationale: "Agent-referenced rules project into the lean .claude/rules/ lane." },
|
|
1207
|
+
effort: { disposition: "mapped", rationale: "Reasoning effort passes through to the Claude effort field." },
|
|
1208
|
+
invoke_subagent: { disposition: "mapped", rationale: "Delegation maps to the Claude Agent tool (allowlist on coordinators)." },
|
|
1209
|
+
send_message: { disposition: "approximated", rationale: "Maps to the Claude SendMessage tool; cross-session reach differs from Antigravity messaging." },
|
|
1210
|
+
manage_task: { disposition: "approximated", rationale: 'Maps to the Claude task tools (TaskCreate/TaskUpdate), but the live tools reference says they are "provided by default only on the models listed under Task tool availability, and on other models when you opt in" \u2014 so the grant can resolve to nothing depending on the model.' },
|
|
1211
|
+
schedule: { disposition: "approximated", rationale: "Maps to Cron tools, but Antigravity reactive liveness timers are event-driven rather than cron-scheduled." },
|
|
1212
|
+
define_subagent: { disposition: "approximated", rationale: "Claude agents are pre-defined: runtime subagent definition is unavailable, so the prompt delegates via the Agent tool." },
|
|
1213
|
+
manage_subagents: { disposition: "approximated", rationale: "Claude manages subagent lifecycle itself; the roster is static and the prompt delegates via the Agent tool." },
|
|
1214
|
+
ask_question: { disposition: "approximated", rationale: "Claude exposes AskUserQuestion only in the main conversation (excluded from subagents by default)." },
|
|
1215
|
+
generate_image: { disposition: "unsupported", rationale: "Claude Code exposes no image-generation tool; asset creation must happen outside the agent run." },
|
|
1216
|
+
skills: { disposition: "degraded", rationale: "Claude subagents discover project skills and can invoke them; the canonical preload list is not injected to avoid paying its token cost on every spawn." },
|
|
1217
|
+
mcpServers: { disposition: "degraded", rationale: "Claude supports per-subagent mcpServers, but the canonical descriptor dialect differs; MCP wiring stays host-configured via the MCP location registry (translation deferred)." }
|
|
1218
|
+
};
|
|
1219
|
+
var MARKER_PROFILE = "claude";
|
|
1220
|
+
var MARKER_PREFIX = "managed-by: agents-united";
|
|
1221
|
+
var HOST = "claude";
|
|
1222
|
+
var ENTRYPOINT_RULES = /* @__PURE__ */ new Set(["CLAUDE.md", "CLAUDE.local.md", "GEMINI.md", "AGENTS.md", "CURSOR.md"]);
|
|
1223
|
+
var COORDINATOR_BASELINE_TOOLS = ["Read", "Write", "Edit", "Bash", "Grep", "Glob"];
|
|
1224
|
+
var READ_ONLY_EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["Write", "Edit", "NotebookEdit", "Bash"]);
|
|
1225
|
+
var FRONTMATTER_REGEX2 = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
1226
|
+
var RUNTIME_NOTE = `## Claude runtime note
|
|
1227
|
+
|
|
1228
|
+
Delegation runs through the Agent tool: the coordinator spawns the specialists named in its own tools
|
|
1229
|
+
allowlist; specialists hold no Agent tool and never spawn peers (the coordinator relays and wakes them). Canonical tool names in this prompt were rewritten to their
|
|
1230
|
+
Claude equivalents; a fenced code block may still show the original spelling because code is preserved
|
|
1231
|
+
byte-for-byte. A subagent does not hand results to a peer: its final report is returned to the
|
|
1232
|
+
conversation that spawned it, and on Claude Code v2.1.271+ in auto mode the runtime delivers it through the
|
|
1233
|
+
SubagentHandback tool.
|
|
1234
|
+
|
|
1235
|
+
Enforced guard: a PreToolUse hook in this file's frontmatter blocks \`git push --force\`, \`.env\` writes and
|
|
1236
|
+
\`vercel --prod\` (exit 2, with the reason); ask the user to run those steps themselves.
|
|
1237
|
+
All other lifecycle hooks described in this prompt are advisory: this host does not fire them.`;
|
|
1238
|
+
var MANAGED_HOOKS = managedGuardHooks();
|
|
1239
|
+
var ClaudeProjector = class _ClaudeProjector {
|
|
1240
|
+
/** Pure data (Plan 017 lifts this object into a shared HostDialectSpec). */
|
|
1241
|
+
static CLAUDE_DIALECT = {
|
|
1242
|
+
id: "claude",
|
|
1243
|
+
nameRegex: /^[a-z0-9]+(-[a-z0-9]+)*$/,
|
|
1244
|
+
toolVocabulary: {
|
|
1245
|
+
view_file: "Read",
|
|
1246
|
+
read_file: "Read",
|
|
1247
|
+
replace_file_content: "Edit",
|
|
1248
|
+
apply_diff: "Edit",
|
|
1249
|
+
multi_replace_file_content: "Edit",
|
|
1250
|
+
write_to_file: "Write",
|
|
1251
|
+
run_command: "Bash",
|
|
1252
|
+
grep_search: "Grep",
|
|
1253
|
+
search_file_content: "Grep",
|
|
1254
|
+
list_dir: "Glob",
|
|
1255
|
+
find_by_name: "Glob",
|
|
1256
|
+
web_search: "WebSearch",
|
|
1257
|
+
search_web: "WebSearch",
|
|
1258
|
+
read_url_content: "WebFetch",
|
|
1259
|
+
invoke_subagent: "Agent",
|
|
1260
|
+
define_subagent: "Agent",
|
|
1261
|
+
manage_subagents: "Agent",
|
|
1262
|
+
send_message: "SendMessage",
|
|
1263
|
+
manage_task: "TaskCreate",
|
|
1264
|
+
schedule: "CronCreate",
|
|
1265
|
+
ask_question: "AskUserQuestion",
|
|
1266
|
+
// The runtime's own hand-off channel for subagents (Claude Code v2.1.271+, auto mode): it delivers
|
|
1267
|
+
// a subagent's final report to whichever conversation receives that subagent's result. It has no
|
|
1268
|
+
// Antigravity-dialect equivalent, so the lane grants it to specialists directly (see renderRole);
|
|
1269
|
+
// the token exists so a canonical agent could also name it explicitly.
|
|
1270
|
+
subagent_handback: "SubagentHandback"
|
|
1271
|
+
},
|
|
1272
|
+
bodyToolVocabulary: {
|
|
1273
|
+
view_file: "Read",
|
|
1274
|
+
read_file: "Read",
|
|
1275
|
+
write_to_file: "Write",
|
|
1276
|
+
replace_file_content: "Edit",
|
|
1277
|
+
apply_diff: "Edit",
|
|
1278
|
+
multi_replace_file_content: "Edit",
|
|
1279
|
+
run_command: "Bash",
|
|
1280
|
+
grep_search: "Grep",
|
|
1281
|
+
find_by_name: "Glob",
|
|
1282
|
+
list_dir: "Glob",
|
|
1283
|
+
search_web: "WebSearch",
|
|
1284
|
+
read_url_content: "WebFetch",
|
|
1285
|
+
invoke_subagent: "Agent",
|
|
1286
|
+
define_subagent: "the Agent tool",
|
|
1287
|
+
manage_subagents: "the Agent tool",
|
|
1288
|
+
// ADR 0018 decision 8 requires canonical tool names in prose to be rewritten, not just the
|
|
1289
|
+
// frontmatter vocabulary: a specialist reading "reach a peer with `send_message`" would otherwise
|
|
1290
|
+
// look for a tool that does not exist on this host. Whole-word and code-fence-aware like the rest.
|
|
1291
|
+
send_message: "SendMessage",
|
|
1292
|
+
manage_task: "TaskCreate",
|
|
1293
|
+
ask_question: "AskUserQuestion",
|
|
1294
|
+
generate_image: "image generation (unsupported on this host)"
|
|
1295
|
+
// `schedule` is deliberately absent: it is an ordinary English word, so rewriting it in prose
|
|
1296
|
+
// would corrupt sentences. Its loss is recorded once at frontmatter level instead.
|
|
1297
|
+
},
|
|
1298
|
+
commandVocabulary: {
|
|
1299
|
+
team_command: "Agent Teams (opt-in: agents start --host claude --teams)",
|
|
1300
|
+
deep_planning_command: "/workflow-grill",
|
|
1301
|
+
interview_command: "/grill-me"
|
|
1302
|
+
},
|
|
1303
|
+
permissionModeMap: {
|
|
1304
|
+
acceptEdits: "acceptEdits",
|
|
1305
|
+
readOnly: "plan",
|
|
1306
|
+
requestReview: "default",
|
|
1307
|
+
strict: "default"
|
|
1308
|
+
},
|
|
1309
|
+
modelMap: { pro: "sonnet", flash: "haiku" },
|
|
1310
|
+
// ADR 0018 decision 7, amended 2026-09-22 by the product owner. The canonical catalog declares
|
|
1311
|
+
// `model: inherit` on all 59 agents, which made `inherit` the de-facto value and left every
|
|
1312
|
+
// projection inheriting the *session* model instead of a chosen posture. The intended posture is
|
|
1313
|
+
// explicit — coordinators reason on Opus at high effort, specialists run on Sonnet at medium
|
|
1314
|
+
// effort — so `inherit` now resolves to a role default rather than being omitted. Both fields are
|
|
1315
|
+
// documented subagent frontmatter: `model` accepts sonnet|opus|haiku|fable|<full id>|inherit, and
|
|
1316
|
+
// `effort` (low|medium|high|xhigh|max) overrides the session effort level. Caveats recorded rather
|
|
1317
|
+
// than coded around: Opus is unavailable on some plans, so a workspace may need a different anchor;
|
|
1318
|
+
// and `xhigh`/`max` exist only on newer models. Both are one-constant changes here.
|
|
1319
|
+
roleModelDefaults: { coordinator: "opus", specialist: "sonnet" },
|
|
1320
|
+
roleEffortDefaults: { coordinator: "high", specialist: "medium" },
|
|
1321
|
+
bodySectionOverrides: [
|
|
1322
|
+
{
|
|
1323
|
+
// Plan 019 escape hatch (2026-09-25): the old anchor — "Subagent Delegation & Host
|
|
1324
|
+
// Routing" — was residue (it described OTHER runtimes: Antigravity's language_server.exe
|
|
1325
|
+
// limitation and Cline's subagent_* tools) and the G1 purge deleted it. Its Claude-side
|
|
1326
|
+
// rendering survives as this per-host overlay, keyed on the neutral "Delegation
|
|
1327
|
+
// Mechanics" stub in canonical; hosts without an overlay render the stub itself.
|
|
1328
|
+
heading: /^#{2,3}\s*.*Delegation Mechanics/m,
|
|
1329
|
+
replacement: [
|
|
1330
|
+
"### \u26A1 Subagent Delegation & Agent Routing (Claude Code)",
|
|
1331
|
+
"",
|
|
1332
|
+
"- **Delegation tool**: spawn specialists with the `Agent` tool. Your `tools` allowlist names exactly the specialist types you may call (the projected definitions in `.claude/agents/`, with the `subagent-` prefix stripped); calling any other type fails, so send work only to those names. There are no `TypeName`/`Role`/`Prompt` fields \u2014 `Agent` takes the specialist type and one self-contained prompt, so put the role, the scope boundaries and the acceptance criteria inside that prompt.",
|
|
1333
|
+
"- **Parallel work**: spawn independent specialists in one turn \u2014 each runs in its own context window and returns its result to you (delivered by `SubagentHandback` on Claude Code v2.1.271+ in auto mode). You are the single synthesis and relay point: name who reported what, and never let one specialist wait on another.",
|
|
1334
|
+
"- **Your allowlist is the whole domain team, not only what is installed.** A listed type whose definition is not in this workspace yet will fail to spawn: say so, recommend installing it (`agents add <addon>`), and handle that slice yourself if the user declines. Never delegate to a type outside your allowlist.",
|
|
1335
|
+
"- **Agent Teams (opt-in, `--teams`)**: the lead can spawn *teammates* instead of subagents and message them directly with `SendMessage`. The scaffold is experimental \u2014 one team per session, a fixed lead, no nested teams \u2014 so never make it load-bearing."
|
|
1336
|
+
].join("\n")
|
|
1337
|
+
}
|
|
1338
|
+
],
|
|
1339
|
+
budgets: { skillDescriptionChars: 1536, agentDescriptionTokens: 15e3 },
|
|
1340
|
+
maxRuleLines: 200
|
|
1341
|
+
};
|
|
1342
|
+
/** `subagent-backend-architect` -> `backend-architect` (Step 0 proved zero collisions). */
|
|
1343
|
+
static stripSubagentPrefix(name) {
|
|
1344
|
+
return name.trim().replace(/^subagent-/, "");
|
|
1345
|
+
}
|
|
1346
|
+
/** Uniform managed-projection marker; always the first line of the projected body. */
|
|
1347
|
+
static marker(canonicalRelPath) {
|
|
1348
|
+
const norm = canonicalRelPath.replace(/\\/g, "/");
|
|
1349
|
+
return `<!-- managed-by: agents-united | profile: ${MARKER_PROFILE} | canonical: ${norm} | do not edit -->`;
|
|
1350
|
+
}
|
|
1351
|
+
/** `generative_ui` -> `generative-ui`: every dialect requires lowercase-hyphen names. */
|
|
1352
|
+
static normalizeSkillName(name) {
|
|
1353
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1354
|
+
}
|
|
1355
|
+
static parse(canonicalContent, canonicalRelPath) {
|
|
1356
|
+
const match = canonicalContent.match(FRONTMATTER_REGEX2);
|
|
1357
|
+
if (!match) {
|
|
1358
|
+
throw new Error(`Cannot project ${canonicalRelPath}: no YAML frontmatter found.`);
|
|
1359
|
+
}
|
|
1360
|
+
let parsed;
|
|
1361
|
+
try {
|
|
1362
|
+
parsed = yaml3.parse(match[1]);
|
|
1363
|
+
} catch (cause) {
|
|
1364
|
+
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
1365
|
+
throw new Error(`Cannot project ${canonicalRelPath}: invalid YAML frontmatter (${err.message}).`);
|
|
1366
|
+
}
|
|
1367
|
+
const meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
1368
|
+
return { meta, body: match[2].replace(/^\r?\n/, "") };
|
|
1369
|
+
}
|
|
1370
|
+
static ledgerEntry(feature) {
|
|
1371
|
+
const known = FEATURE_LEDGER[feature];
|
|
1372
|
+
if (!known) return void 0;
|
|
1373
|
+
return { feature, host: HOST, disposition: known.disposition, rationale: known.rationale };
|
|
1374
|
+
}
|
|
1375
|
+
/** Guard: an unmapped feature must never be dropped without a recorded disposition. */
|
|
1376
|
+
static requireDisposition(feature, canonicalRelPath) {
|
|
1377
|
+
const entry = _ClaudeProjector.ledgerEntry(feature);
|
|
1378
|
+
if (!entry) {
|
|
1379
|
+
throw new Error(
|
|
1380
|
+
`No translation-ledger disposition recorded for feature "${feature}" in ${canonicalRelPath}. Add it to FEATURE_LEDGER so translation loss is never silent (ADR 0018 decision 9).`
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
return entry;
|
|
1384
|
+
}
|
|
1385
|
+
/**
|
|
1386
|
+
* Replace one canonical body section, matched by its heading, with a host-native rendering.
|
|
1387
|
+
*
|
|
1388
|
+
* Deterministic: the section runs from the matched heading to the next heading of the same or a higher
|
|
1389
|
+
* level (an `h3` stops at the next `h1`/`h2`), and trailing blank lines and the `---` separator are left
|
|
1390
|
+
* with the following section so the document structure is unchanged. A heading that is absent is a no-op.
|
|
1391
|
+
*/
|
|
1392
|
+
static replaceSection(body, heading, replacement) {
|
|
1393
|
+
const lines = body.split("\n");
|
|
1394
|
+
const start = lines.findIndex((line) => heading.test(line));
|
|
1395
|
+
if (start < 0) return body;
|
|
1396
|
+
let end = lines.length;
|
|
1397
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
1398
|
+
if (/^#{1,2}\s/.test(lines[i])) {
|
|
1399
|
+
end = i;
|
|
1400
|
+
break;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
while (end > start + 1 && (lines[end - 1].trim() === "" || lines[end - 1].trim() === "---")) end -= 1;
|
|
1404
|
+
return [...lines.slice(0, start), ...replacement.split("\n"), ...lines.slice(end)].join("\n");
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Deterministically rewrite canonical tool names in prompt prose (whole-word, case-sensitive,
|
|
1408
|
+
* fenced code blocks byte-preserved) and report a disposition for every rewrite that loses fidelity.
|
|
1409
|
+
*/
|
|
1410
|
+
/** Command tokens are ledger citizens (Plan 020 note 7) — rewritten and dispositioned. */
|
|
1411
|
+
static COMMAND_LEDGER = {
|
|
1412
|
+
team_command: { disposition: "mapped", rationale: "Rewritten to the host team entrypoint (Agent Teams opt-in)." },
|
|
1413
|
+
deep_planning_command: { disposition: "mapped", rationale: "Rewritten to the host deep-planning command." },
|
|
1414
|
+
interview_command: { disposition: "mapped", rationale: "Rewritten to the host interview command." }
|
|
1415
|
+
};
|
|
1416
|
+
/** Plan 019 safety net: residue-headed sections never survive into a projection body. */
|
|
1417
|
+
static dropResidueSections(body) {
|
|
1418
|
+
let out = body;
|
|
1419
|
+
for (const pattern of RESIDUE_PATTERNS_BY_HOST.claude) {
|
|
1420
|
+
out = out.replace(
|
|
1421
|
+
new RegExp(`(^|\\n)#{1,6}[^\\n]*${pattern.source}[^\\n]*\\n[\\s\\S]*?(?=\\n#{1,6} |$)`, "g"),
|
|
1422
|
+
"$1"
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1427
|
+
/** Plan 019 safety net: residual residue phrases in prose are neutralized deterministically. */
|
|
1428
|
+
static scrubResiduePhrases(body) {
|
|
1429
|
+
return body.replace(/Cline & CLI/g, "other hosts").replace(/language_server(\.exe)?/g, "the runtime").replace(/Nested Subagent Delegation/g, "peer delegation").replace(/Host Routing/g, "host delegation");
|
|
1430
|
+
}
|
|
1431
|
+
/** Whole-word canonical tokens surviving OUTSIDE code fences after the rewrite pass. */
|
|
1432
|
+
static survivingBodyTokens(body) {
|
|
1433
|
+
const vocab = {
|
|
1434
|
+
..._ClaudeProjector.CLAUDE_DIALECT.toolVocabulary,
|
|
1435
|
+
..._ClaudeProjector.CLAUDE_DIALECT.commandVocabulary
|
|
1436
|
+
};
|
|
1437
|
+
const prose = body.split(/(```[\s\S]*?```)/g).filter((_, index) => index % 2 === 0).join("\n");
|
|
1438
|
+
return Object.keys(vocab).filter(
|
|
1439
|
+
(token) => new RegExp(`(^|[^A-Za-z0-9_])${token}(?=[^A-Za-z0-9_]|$)`).test(prose)
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
static rewriteBody(body) {
|
|
1443
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
1444
|
+
body = _ClaudeProjector.dropResidueSections(body);
|
|
1445
|
+
body = _ClaudeProjector.scrubResiduePhrases(body);
|
|
1446
|
+
for (const override of _ClaudeProjector.CLAUDE_DIALECT.bodySectionOverrides) {
|
|
1447
|
+
body = _ClaudeProjector.replaceSection(body, override.heading, override.replacement);
|
|
1448
|
+
}
|
|
1449
|
+
const vocab = {
|
|
1450
|
+
..._ClaudeProjector.CLAUDE_DIALECT.bodyToolVocabulary,
|
|
1451
|
+
..._ClaudeProjector.CLAUDE_DIALECT.commandVocabulary
|
|
1452
|
+
};
|
|
1453
|
+
const tokens = Object.keys(vocab).sort((a, b) => b.length - a.length);
|
|
1454
|
+
const parts = body.split(/(```[\s\S]*?```)/g);
|
|
1455
|
+
const rewritten = parts.map((part, index) => {
|
|
1456
|
+
if (index % 2 === 1) return part;
|
|
1457
|
+
let text2 = part;
|
|
1458
|
+
for (const token of tokens) {
|
|
1459
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_])${token}(?=[^A-Za-z0-9_]|$)`, "g");
|
|
1460
|
+
if (!text2.match(re)) continue;
|
|
1461
|
+
const command = _ClaudeProjector.COMMAND_LEDGER[token];
|
|
1462
|
+
const entry = command ? { feature: token, host: HOST, disposition: command.disposition, rationale: command.rationale } : _ClaudeProjector.ledgerEntry(token);
|
|
1463
|
+
if (entry) ledger.set(token, entry);
|
|
1464
|
+
text2 = text2.replace(re, `$1${vocab[token]}`);
|
|
1465
|
+
}
|
|
1466
|
+
return text2;
|
|
1467
|
+
});
|
|
1468
|
+
return { body: rewritten.join(""), ledger: [...ledger.values()] };
|
|
1469
|
+
}
|
|
1470
|
+
/** Render `.claude/agents/<role>.md` from a canonical agent definition. */
|
|
1471
|
+
static renderRole(canonicalContent, canonicalRelPath, opts = {}) {
|
|
1472
|
+
const { meta, body } = _ClaudeProjector.parse(canonicalContent, canonicalRelPath);
|
|
1473
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
1474
|
+
for (const key of Object.keys(meta)) {
|
|
1475
|
+
if (INLINE_KEYS.has(key)) continue;
|
|
1476
|
+
const entry = _ClaudeProjector.requireDisposition(key, canonicalRelPath);
|
|
1477
|
+
ledger.set(entry.feature, entry);
|
|
1478
|
+
}
|
|
1479
|
+
const rawName = typeof meta.name === "string" && meta.name.trim().length > 0 ? meta.name.trim() : path5.basename(canonicalRelPath).replace(/\.md$/i, "");
|
|
1480
|
+
const out = { name: _ClaudeProjector.stripSubagentPrefix(rawName) };
|
|
1481
|
+
if (typeof meta.description === "string" && meta.description.trim().length > 0) {
|
|
1482
|
+
out.description = meta.description.trim();
|
|
1483
|
+
}
|
|
1484
|
+
const isCoordinator = !!(opts.allowlist && opts.allowlist.length > 0) || meta.type === "orchestrator" || meta.mainAgent === true;
|
|
1485
|
+
const tools = [];
|
|
1486
|
+
if (isCoordinator) {
|
|
1487
|
+
tools.push(opts.allowlist && opts.allowlist.length > 0 ? `Agent(${opts.allowlist.join(", ")})` : "Agent");
|
|
1488
|
+
}
|
|
1489
|
+
for (const raw of Array.isArray(meta.tools) ? meta.tools : []) {
|
|
1490
|
+
if (typeof raw !== "string") continue;
|
|
1491
|
+
const entry = _ClaudeProjector.ledgerEntry(raw);
|
|
1492
|
+
if (entry) ledger.set(entry.feature, entry);
|
|
1493
|
+
const mapped = _ClaudeProjector.CLAUDE_DIALECT.toolVocabulary[raw];
|
|
1494
|
+
if (!mapped) {
|
|
1495
|
+
_ClaudeProjector.requireDisposition(raw, canonicalRelPath);
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (!tools.includes(mapped)) tools.push(mapped);
|
|
1499
|
+
}
|
|
1500
|
+
if (opts.allowlist && opts.allowlist.length > 0) {
|
|
1501
|
+
const withoutBareAgent = tools.filter((t) => t !== "Agent");
|
|
1502
|
+
tools.length = 0;
|
|
1503
|
+
tools.push(...withoutBareAgent);
|
|
1504
|
+
}
|
|
1505
|
+
if (!isCoordinator) {
|
|
1506
|
+
const withoutAgent = tools.filter((t) => t !== "Agent" && !t.startsWith("Agent("));
|
|
1507
|
+
tools.length = 0;
|
|
1508
|
+
tools.push(...withoutAgent);
|
|
1509
|
+
}
|
|
1510
|
+
if (isCoordinator) {
|
|
1511
|
+
for (const base of COORDINATOR_BASELINE_TOOLS) {
|
|
1512
|
+
if (!tools.includes(base)) tools.push(base);
|
|
1513
|
+
}
|
|
1514
|
+
} else if (!tools.includes("SubagentHandback")) {
|
|
1515
|
+
tools.push("SubagentHandback");
|
|
1516
|
+
}
|
|
1517
|
+
const permission = typeof meta.permissionMode === "string" ? _ClaudeProjector.CLAUDE_DIALECT.permissionModeMap[meta.permissionMode] ?? "default" : void 0;
|
|
1518
|
+
if (permission === "plan") {
|
|
1519
|
+
const restricted = tools.filter((t) => !READ_ONLY_EXCLUDED_TOOLS.has(t));
|
|
1520
|
+
tools.length = 0;
|
|
1521
|
+
tools.push(...restricted);
|
|
1522
|
+
}
|
|
1523
|
+
out.tools = tools;
|
|
1524
|
+
if (permission) out.permissionMode = permission;
|
|
1525
|
+
const declaredModel = typeof meta.model === "string" ? meta.model : void 0;
|
|
1526
|
+
const mappedModel = declaredModel && declaredModel !== "inherit" ? _ClaudeProjector.CLAUDE_DIALECT.modelMap[declaredModel] ?? declaredModel : void 0;
|
|
1527
|
+
out.model = mappedModel ?? (isCoordinator ? _ClaudeProjector.CLAUDE_DIALECT.roleModelDefaults.coordinator : _ClaudeProjector.CLAUDE_DIALECT.roleModelDefaults.specialist);
|
|
1528
|
+
const declaredEffort = typeof meta.effort === "string" ? meta.effort : void 0;
|
|
1529
|
+
out.effort = declaredEffort ?? (isCoordinator ? _ClaudeProjector.CLAUDE_DIALECT.roleEffortDefaults.coordinator : _ClaudeProjector.CLAUDE_DIALECT.roleEffortDefaults.specialist);
|
|
1530
|
+
if (typeof opts.maxTurns === "number") out.maxTurns = opts.maxTurns;
|
|
1531
|
+
out.hooks = MANAGED_HOOKS;
|
|
1532
|
+
const rewritten = _ClaudeProjector.rewriteBody(body);
|
|
1533
|
+
for (const entry of rewritten.ledger) ledger.set(entry.feature, entry);
|
|
1534
|
+
for (const token of _ClaudeProjector.survivingBodyTokens(rewritten.body)) {
|
|
1535
|
+
_ClaudeProjector.requireDisposition(token, canonicalRelPath);
|
|
1536
|
+
}
|
|
1537
|
+
if (meta.projections !== void 0 && meta.projections !== null) {
|
|
1538
|
+
validateProjectionOverlays(meta.projections, { id: HOST, fields: CLAUDE_FIELD_POLICY });
|
|
1539
|
+
const overlay = meta.projections[HOST];
|
|
1540
|
+
if (overlay) Object.assign(out, overlay);
|
|
1541
|
+
}
|
|
1542
|
+
const yamlStr = yaml3.stringify(out).replace(/\r\n/g, "\n").trimEnd();
|
|
1543
|
+
const content = `---
|
|
1544
|
+
${yamlStr}
|
|
1545
|
+
---
|
|
1546
|
+
${_ClaudeProjector.marker(canonicalRelPath)}
|
|
1547
|
+
|
|
1548
|
+
${RUNTIME_NOTE}
|
|
1549
|
+
|
|
1550
|
+
${rewritten.body.trim()}
|
|
1551
|
+
`;
|
|
1552
|
+
return { content, ledger: [...ledger.values()] };
|
|
1553
|
+
}
|
|
1554
|
+
/** Render `.claude/skills/<name>/SKILL.md` from a canonical skill. */
|
|
1555
|
+
static renderSkill(canonicalContent, canonicalRelPath) {
|
|
1556
|
+
const { meta, body } = _ClaudeProjector.parse(canonicalContent, canonicalRelPath);
|
|
1557
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
1558
|
+
const rawName = typeof meta.name === "string" && meta.name.trim().length > 0 ? meta.name.trim() : path5.basename(path5.dirname(canonicalRelPath));
|
|
1559
|
+
const normalized = _ClaudeProjector.normalizeSkillName(rawName);
|
|
1560
|
+
if (normalized !== rawName) {
|
|
1561
|
+
ledger.set(`skill-name:${rawName}`, {
|
|
1562
|
+
feature: `skill-name:${rawName}`,
|
|
1563
|
+
host: HOST,
|
|
1564
|
+
disposition: "mapped",
|
|
1565
|
+
rationale: `Skill name normalized to the dialect rule (${rawName} -> ${normalized}); body references follow the artifact name.`
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
const out = { name: normalized };
|
|
1569
|
+
if (typeof meta.description === "string" && meta.description.trim().length > 0) {
|
|
1570
|
+
out.description = meta.description.trim();
|
|
1571
|
+
}
|
|
1572
|
+
if (meta["disable-slash-command"] === true || meta.disableSlashCommand === true) {
|
|
1573
|
+
out["user-invocable"] = false;
|
|
1574
|
+
ledger.set("disable-slash-command", {
|
|
1575
|
+
feature: "disable-slash-command",
|
|
1576
|
+
host: HOST,
|
|
1577
|
+
disposition: "mapped",
|
|
1578
|
+
rationale: "Hidden from the / palette while staying model-invocable (Claude polarity: user-invocable, never disable-model-invocation)."
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
const metadata = meta.metadata;
|
|
1582
|
+
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
|
1583
|
+
const allowed = ["author", "version", "icon", "source", "license"];
|
|
1584
|
+
const kept = {};
|
|
1585
|
+
for (const key of allowed) {
|
|
1586
|
+
const value = metadata[key];
|
|
1587
|
+
if (value !== void 0) kept[key] = value;
|
|
1588
|
+
}
|
|
1589
|
+
for (const key of Object.keys(metadata)) {
|
|
1590
|
+
if (allowed.includes(key)) continue;
|
|
1591
|
+
ledger.set(`metadata:${key}`, {
|
|
1592
|
+
feature: `metadata:${key}`,
|
|
1593
|
+
host: HOST,
|
|
1594
|
+
disposition: "mapped",
|
|
1595
|
+
rationale: "Non-standard skill metadata; stripped because the projection carries only the Agent Skills contract."
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
if (Object.keys(kept).length > 0) out.metadata = kept;
|
|
1599
|
+
}
|
|
1600
|
+
if (typeof meta.license === "string") out.license = meta.license;
|
|
1601
|
+
const HANDLED_SKILL_KEYS = /* @__PURE__ */ new Set([
|
|
1602
|
+
"name",
|
|
1603
|
+
"description",
|
|
1604
|
+
"metadata",
|
|
1605
|
+
"license",
|
|
1606
|
+
"disable-slash-command",
|
|
1607
|
+
"disableSlashCommand"
|
|
1608
|
+
]);
|
|
1609
|
+
for (const key of Object.keys(meta)) {
|
|
1610
|
+
if (HANDLED_SKILL_KEYS.has(key)) continue;
|
|
1611
|
+
const entry = _ClaudeProjector.requireDisposition(key, canonicalRelPath);
|
|
1612
|
+
ledger.set(entry.feature, entry);
|
|
1613
|
+
}
|
|
1614
|
+
let rewrittenBody = body;
|
|
1615
|
+
if (normalized !== rawName) {
|
|
1616
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_-])${rawName}(?![A-Za-z0-9_-])`, "gi");
|
|
1617
|
+
rewrittenBody = rewrittenBody.replace(re, `$1${normalized}`);
|
|
1618
|
+
}
|
|
1619
|
+
const rewritten = _ClaudeProjector.rewriteBody(rewrittenBody);
|
|
1620
|
+
for (const entry of rewritten.ledger) ledger.set(entry.feature, entry);
|
|
1621
|
+
const yamlStr = yaml3.stringify(out).replace(/\r\n/g, "\n").trimEnd();
|
|
1622
|
+
const content = `---
|
|
1623
|
+
${yamlStr}
|
|
1624
|
+
---
|
|
1625
|
+
${_ClaudeProjector.marker(canonicalRelPath)}
|
|
1626
|
+
|
|
1627
|
+
${rewritten.body.trim()}
|
|
1628
|
+
`;
|
|
1629
|
+
return { content, ledger: [...ledger.values()] };
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Render `.claude/rules/<rule>.md`. Rules load unconditionally in every Claude session, so this
|
|
1633
|
+
* lane is deliberately lean: optional `paths:` scoping plus a hard adherence budget (ADR 0018).
|
|
1634
|
+
*
|
|
1635
|
+
* The `paths:` block is the ONLY frontmatter field this lane emits, so it is emitted only when a
|
|
1636
|
+
* rule is actually scoped. The live doc is explicit — "Rules without a `paths` field are loaded
|
|
1637
|
+
* unconditionally and apply to all files" — which makes frontmatter optional; an unscoped rule is
|
|
1638
|
+
* therefore rendered marker-first with no frontmatter block at all, matching the Cline lane's shape
|
|
1639
|
+
* instead of emitting a stray empty `--- {} ---`.
|
|
1640
|
+
*/
|
|
1641
|
+
static renderRule(canonicalContent, canonicalRelPath, opts = {}) {
|
|
1642
|
+
const lineCount = canonicalContent.split(/\r?\n/).length;
|
|
1643
|
+
if (lineCount > _ClaudeProjector.CLAUDE_DIALECT.maxRuleLines) {
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`Rule ${canonicalRelPath} has ${lineCount} lines, exceeding the ${_ClaudeProjector.CLAUDE_DIALECT.maxRuleLines}-line adherence budget (ADR 0018 decision 5).`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
const frontmatter = {};
|
|
1649
|
+
if (opts.paths && opts.paths.length > 0) frontmatter.paths = opts.paths;
|
|
1650
|
+
const head = Object.keys(frontmatter).length > 0 ? `---
|
|
1651
|
+
${yaml3.stringify(frontmatter).replace(/\r\n/g, "\n").trimEnd()}
|
|
1652
|
+
---
|
|
1653
|
+
` : "";
|
|
1654
|
+
const rewritten = _ClaudeProjector.rewriteBody(canonicalContent.trim());
|
|
1655
|
+
const content = `${head}${_ClaudeProjector.marker(canonicalRelPath)}
|
|
1656
|
+
|
|
1657
|
+
${rewritten.body}
|
|
1658
|
+
`;
|
|
1659
|
+
return { content, ledger: rewritten.ledger };
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* The specialist roster a coordinator may delegate to: the whole **domain** team, not only the slice
|
|
1663
|
+
* this bundle declares or what happens to be installed.
|
|
1664
|
+
*
|
|
1665
|
+
* `software-engineering` declares 4 specialists, but the `engineering` domain has 15 — so an essentials
|
|
1666
|
+
* install used to leave the domain lead able to call 4 of its 15 people, with the list growing only when
|
|
1667
|
+
* the operator happened to install more *and* re-project. `bundles.json` already groups every bundle under
|
|
1668
|
+
* a `domain`, so the roster is the union of that domain's declared agents minus the coordinator itself.
|
|
1669
|
+
* The allowlist is then stable (independent of install state) and complete (it never silently hides a
|
|
1670
|
+
* teammate). The cost is that a listed type may not be installed yet — the delegation section turns that
|
|
1671
|
+
* into an install recommendation rather than a dead end.
|
|
1672
|
+
*
|
|
1673
|
+
* Sorted for determinism: repeated projections must be byte-identical, because `agents doctor` diffs them.
|
|
1674
|
+
*/
|
|
1675
|
+
static resolveDomainRoster(bundle, coordinatorFile, registryDir) {
|
|
1676
|
+
const names = /* @__PURE__ */ new Set();
|
|
1677
|
+
try {
|
|
1678
|
+
const raw = fs3.readFileSync(path5.join(registryDir, "bundles.json"), "utf8");
|
|
1679
|
+
const bundles = JSON.parse(raw).bundles ?? {};
|
|
1680
|
+
for (const candidate of Object.values(bundles)) {
|
|
1681
|
+
if (!bundle.domain || candidate.domain !== bundle.domain) continue;
|
|
1682
|
+
for (const agentFile of candidate.agents || []) {
|
|
1683
|
+
if (agentFile === coordinatorFile) continue;
|
|
1684
|
+
names.add(_ClaudeProjector.stripSubagentPrefix(agentFile.replace(/\.md$/i, "")));
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
return [...names].sort();
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Plan the full Claude compound projection for one bundle: roles (with a coordinator allowlist of
|
|
1693
|
+
* its whole domain team), skills (normalized names plus auxiliary files copied byte-for-byte), and
|
|
1694
|
+
* the lean deduplicated rule set. It emits **no** team-manifest duplicate and **no** workflows lane,
|
|
1695
|
+
* because Claude workflows are JavaScript orchestration scripts rather than markdown (ADR 0018 d14).
|
|
1696
|
+
*/
|
|
1697
|
+
static async planCompoundProjection(bundle, scope, resolved, registryDir, _excludeAddons) {
|
|
1698
|
+
void scope;
|
|
1699
|
+
const artifacts = [];
|
|
1700
|
+
const coordinatorFile = bundle.orchestrator || `${bundle.name}.md`;
|
|
1701
|
+
const domainRoster = _ClaudeProjector.resolveDomainRoster(bundle, coordinatorFile, registryDir);
|
|
1702
|
+
const specialistNames = domainRoster.length > 0 ? domainRoster : (bundle.agents || []).map((f) => _ClaudeProjector.stripSubagentPrefix(f.replace(/\.md$/i, "")));
|
|
1703
|
+
const maxTurns = bundle.planningLoop?.budget?.maxIterations;
|
|
1704
|
+
for (const agentFile of resolved.agents || []) {
|
|
1705
|
+
const canonicalRel = `agents/${agentFile}`;
|
|
1706
|
+
const src = path5.join(registryDir, "agents", agentFile);
|
|
1707
|
+
if (!await fs3.pathExists(src)) continue;
|
|
1708
|
+
const content = await fs3.readFile(src, "utf8");
|
|
1709
|
+
const isCoordinator = coordinatorFile === agentFile;
|
|
1710
|
+
const roleName = _ClaudeProjector.stripSubagentPrefix(agentFile.replace(/\.md$/i, ""));
|
|
1711
|
+
const rendered = _ClaudeProjector.renderRole(content, canonicalRel, {
|
|
1712
|
+
allowlist: isCoordinator ? specialistNames : void 0,
|
|
1713
|
+
maxTurns: isCoordinator ? maxTurns : void 0
|
|
1714
|
+
});
|
|
1715
|
+
artifacts.push({
|
|
1716
|
+
kind: "role",
|
|
1717
|
+
canonical: canonicalRel,
|
|
1718
|
+
relPath: `.claude/agents/${roleName}.md`,
|
|
1719
|
+
content: rendered.content,
|
|
1720
|
+
managedMarker: true
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
for (const skillName of resolved.skills || []) {
|
|
1724
|
+
const skillDir = path5.join(registryDir, "skills", skillName);
|
|
1725
|
+
const skillFile = path5.join(skillDir, "SKILL.md");
|
|
1726
|
+
if (!await fs3.pathExists(skillFile)) continue;
|
|
1727
|
+
const canonicalRel = `skills/${skillName}/SKILL.md`;
|
|
1728
|
+
const normalized = _ClaudeProjector.normalizeSkillName(skillName);
|
|
1729
|
+
artifacts.push({
|
|
1730
|
+
kind: "skill",
|
|
1731
|
+
canonical: canonicalRel,
|
|
1732
|
+
relPath: `.claude/skills/${normalized}/SKILL.md`,
|
|
1733
|
+
content: _ClaudeProjector.renderSkill(await fs3.readFile(skillFile, "utf8"), canonicalRel).content,
|
|
1734
|
+
managedMarker: true
|
|
1735
|
+
});
|
|
1736
|
+
const entries = (await fs3.readdir(skillDir, { withFileTypes: true })).filter((entry) => entry.name !== "SKILL.md").sort((a, b) => a.name.localeCompare(b.name));
|
|
1737
|
+
for (const entry of entries) {
|
|
1738
|
+
if (!entry.isFile()) continue;
|
|
1739
|
+
artifacts.push({
|
|
1740
|
+
kind: "skill",
|
|
1741
|
+
canonical: `skills/${skillName}/${entry.name}`,
|
|
1742
|
+
relPath: `.claude/skills/${normalized}/${entry.name}`,
|
|
1743
|
+
sourceFilePath: path5.join(skillDir, entry.name),
|
|
1744
|
+
managedMarker: false
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
for (const ruleFile of [...new Set(resolved.rules || [])].sort()) {
|
|
1749
|
+
if (ENTRYPOINT_RULES.has(ruleFile)) continue;
|
|
1750
|
+
const src = path5.join(registryDir, "rules", ruleFile);
|
|
1751
|
+
if (!await fs3.pathExists(src)) continue;
|
|
1752
|
+
const canonicalRel = `rules/${ruleFile}`;
|
|
1753
|
+
const content = await fs3.readFile(src, "utf8");
|
|
1754
|
+
if (content.split(/\r?\n/).length > _ClaudeProjector.CLAUDE_DIALECT.maxRuleLines) continue;
|
|
1755
|
+
artifacts.push({
|
|
1756
|
+
kind: "rule",
|
|
1757
|
+
canonical: canonicalRel,
|
|
1758
|
+
relPath: `.claude/rules/${ruleFile}`,
|
|
1759
|
+
content: _ClaudeProjector.renderRule(content, canonicalRel).content,
|
|
1760
|
+
managedMarker: true
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
return artifacts;
|
|
1764
|
+
}
|
|
1765
|
+
/** Claude plugin package name rule; every bundle in `registry/bundles.json` satisfies it today. */
|
|
1766
|
+
static PLUGIN_NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
1767
|
+
/**
|
|
1768
|
+
* Render the Claude plugin manifest (`.claude-plugin/plugin.json`).
|
|
1769
|
+
*
|
|
1770
|
+
* Deterministic: fixed field order, two-space indent, trailing newline, and no data read
|
|
1771
|
+
* from the environment (a `package.json` lookup would make the bytes differ between a
|
|
1772
|
+
* project and a global install and would trip doctor's recorded-hash drift check).
|
|
1773
|
+
*/
|
|
1774
|
+
static renderPluginManifest(bundle, canonicalRelPath) {
|
|
1775
|
+
if (!_ClaudeProjector.PLUGIN_NAME_REGEX.test(bundle.name)) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`Cannot project ${canonicalRelPath}: bundle name "${bundle.name}" is not a valid Claude plugin name (must match ^[a-z0-9][a-z0-9-]*$).`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
const markerText = `${MARKER_PREFIX} | profile: ${MARKER_PROFILE} | do not edit`;
|
|
1781
|
+
const description = bundle.description ? `${bundle.description} [${markerText}]` : markerText;
|
|
1782
|
+
const manifest = {
|
|
1783
|
+
author: "",
|
|
1784
|
+
description,
|
|
1785
|
+
homepage: "",
|
|
1786
|
+
license: "",
|
|
1787
|
+
name: bundle.name,
|
|
1788
|
+
repository: "",
|
|
1789
|
+
version: bundle.version || "1.0.0"
|
|
1790
|
+
};
|
|
1791
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* ADR 0018 decision 12 / Plan 016 decision 13 — the opt-in Claude **plugin lane**.
|
|
1795
|
+
*
|
|
1796
|
+
* Plans the `claude --plugin-dir` consumable package *inside* the organization package:
|
|
1797
|
+
* `.agents/plugins/<bundle>/.claude-plugin/plugin.json` plus an `agents/` subdirectory that
|
|
1798
|
+
* mirrors the `.claude/agents/` projection name-for-name and byte-for-byte (same `renderRole`
|
|
1799
|
+
* call, same coordinator allowlist, same `maxTurns`).
|
|
1800
|
+
*
|
|
1801
|
+
* **Distribution-only, never the behavioural source.** Claude has no project-local plugin
|
|
1802
|
+
* auto-discovery, plugin agents are namespaced (`plugin:agent`) and a plugin
|
|
1803
|
+
* `permissionMode` is ignored — so the `.claude/agents/` projection produced by
|
|
1804
|
+
* `planCompoundProjection` stays the single behavioural source. This package exists purely
|
|
1805
|
+
* so the same folder can be zipped and consumed elsewhere via `--plugin-dir`.
|
|
1806
|
+
*
|
|
1807
|
+
* Additive and caller-gated (`InstallOptions.pluginLane`): when the flag is off this method
|
|
1808
|
+
* is never called and the install plan is byte-identical to before.
|
|
1809
|
+
*/
|
|
1810
|
+
static async planPluginLane(bundle, resolved, registryDir) {
|
|
1811
|
+
const artifacts = [];
|
|
1812
|
+
const baseDir = `.agents/plugins/${bundle.name}`;
|
|
1813
|
+
artifacts.push({
|
|
1814
|
+
kind: "plugin-manifest",
|
|
1815
|
+
relPath: `${baseDir}/.claude-plugin/plugin.json`,
|
|
1816
|
+
content: _ClaudeProjector.renderPluginManifest(bundle, `${baseDir}/.claude-plugin/plugin.json`),
|
|
1817
|
+
managedMarker: true,
|
|
1818
|
+
distributionOnly: true
|
|
1819
|
+
});
|
|
1820
|
+
const coordinatorFile = bundle.orchestrator || `${bundle.name}.md`;
|
|
1821
|
+
const domainRoster = _ClaudeProjector.resolveDomainRoster(bundle, coordinatorFile, registryDir);
|
|
1822
|
+
const specialistNames = domainRoster.length > 0 ? domainRoster : (bundle.agents || []).map((f) => _ClaudeProjector.stripSubagentPrefix(f.replace(/\.md$/i, "")));
|
|
1823
|
+
const maxTurns = bundle.planningLoop?.budget?.maxIterations;
|
|
1824
|
+
for (const agentFile of resolved.agents || []) {
|
|
1825
|
+
const canonicalRel = `agents/${agentFile}`;
|
|
1826
|
+
const src = path5.join(registryDir, "agents", agentFile);
|
|
1827
|
+
if (!await fs3.pathExists(src)) continue;
|
|
1828
|
+
const isCoordinator = coordinatorFile === agentFile;
|
|
1829
|
+
const roleName = _ClaudeProjector.stripSubagentPrefix(agentFile.replace(/\.md$/i, ""));
|
|
1830
|
+
const rendered = _ClaudeProjector.renderRole(await fs3.readFile(src, "utf8"), canonicalRel, {
|
|
1831
|
+
allowlist: isCoordinator ? specialistNames : void 0,
|
|
1832
|
+
maxTurns: isCoordinator ? maxTurns : void 0
|
|
1833
|
+
});
|
|
1834
|
+
artifacts.push({
|
|
1835
|
+
kind: "role",
|
|
1836
|
+
canonical: canonicalRel,
|
|
1837
|
+
relPath: `${baseDir}/agents/${roleName}.md`,
|
|
1838
|
+
content: rendered.content,
|
|
1839
|
+
managedMarker: true,
|
|
1840
|
+
distributionOnly: true
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
return artifacts;
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1847
|
+
// src/core/installer.ts
|
|
1848
|
+
import YAML2 from "yaml";
|
|
1849
|
+
|
|
1850
|
+
// src/core/types.ts
|
|
1851
|
+
function assetOwners(rec) {
|
|
1852
|
+
if (!rec) return [];
|
|
1853
|
+
const declared = rec.owners ?? [];
|
|
1854
|
+
if (declared.length > 0) return declared;
|
|
1855
|
+
return rec.bundle ? [rec.bundle] : [];
|
|
1856
|
+
}
|
|
1857
|
+
function mergeAssetOwners(rec, owner) {
|
|
1858
|
+
const owners = assetOwners(rec);
|
|
1859
|
+
if (!owner) return owners;
|
|
1860
|
+
return owners.includes(owner) ? owners : [...owners, owner];
|
|
1861
|
+
}
|
|
1862
|
+
|
|
923
1863
|
// src/core/installer.ts
|
|
924
1864
|
var InstallEngine = class _InstallEngine {
|
|
925
1865
|
registry;
|
|
@@ -927,13 +1867,13 @@ var InstallEngine = class _InstallEngine {
|
|
|
927
1867
|
this.registry = registry2 || new RegistryResolver();
|
|
928
1868
|
}
|
|
929
1869
|
async calculateHash(filePath) {
|
|
930
|
-
const buffer = await
|
|
1870
|
+
const buffer = await fs4.readFile(filePath);
|
|
931
1871
|
return "sha256:" + crypto.createHash("sha256").update(buffer).digest("hex");
|
|
932
1872
|
}
|
|
933
1873
|
async readLockfile(lockfilePath) {
|
|
934
|
-
if (await
|
|
1874
|
+
if (await fs4.pathExists(lockfilePath)) {
|
|
935
1875
|
try {
|
|
936
|
-
return await
|
|
1876
|
+
return await fs4.readJson(lockfilePath);
|
|
937
1877
|
} catch {
|
|
938
1878
|
}
|
|
939
1879
|
}
|
|
@@ -976,24 +1916,24 @@ var InstallEngine = class _InstallEngine {
|
|
|
976
1916
|
return options.scope || "project";
|
|
977
1917
|
}
|
|
978
1918
|
async deployFile(src, dest, method, force) {
|
|
979
|
-
await
|
|
980
|
-
if (await
|
|
1919
|
+
await fs4.ensureDir(path6.dirname(dest));
|
|
1920
|
+
if (await fs4.pathExists(dest) || await fs4.pathExists(dest).catch(() => false)) {
|
|
981
1921
|
if (force) {
|
|
982
|
-
await
|
|
1922
|
+
await fs4.remove(dest);
|
|
983
1923
|
}
|
|
984
1924
|
}
|
|
985
1925
|
if (method === "symlink") {
|
|
986
1926
|
try {
|
|
987
|
-
const stat = await
|
|
1927
|
+
const stat = await fs4.stat(src);
|
|
988
1928
|
const type = stat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
989
|
-
await
|
|
1929
|
+
await fs4.ensureSymlink(src, dest, type);
|
|
990
1930
|
return "symlink";
|
|
991
1931
|
} catch {
|
|
992
|
-
await
|
|
1932
|
+
await fs4.copy(src, dest, { overwrite: true });
|
|
993
1933
|
return "copy";
|
|
994
1934
|
}
|
|
995
1935
|
}
|
|
996
|
-
await
|
|
1936
|
+
await fs4.copy(src, dest, { overwrite: true });
|
|
997
1937
|
return "copy";
|
|
998
1938
|
}
|
|
999
1939
|
/**
|
|
@@ -1001,8 +1941,8 @@ var InstallEngine = class _InstallEngine {
|
|
|
1001
1941
|
* copies of canonical assets, so their bytes differ from the source.
|
|
1002
1942
|
*/
|
|
1003
1943
|
async deployProjection(dest, content) {
|
|
1004
|
-
await
|
|
1005
|
-
await
|
|
1944
|
+
await fs4.ensureDir(path6.dirname(dest));
|
|
1945
|
+
await fs4.writeFile(dest, content, "utf8");
|
|
1006
1946
|
}
|
|
1007
1947
|
/**
|
|
1008
1948
|
* Computes the canonical `.agents/` target dir and the workspace root that contains
|
|
@@ -1010,27 +1950,27 @@ var InstallEngine = class _InstallEngine {
|
|
|
1010
1950
|
*/
|
|
1011
1951
|
static projectionRoot(scope, targetDir) {
|
|
1012
1952
|
const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", targetDir);
|
|
1013
|
-
return { agentsTarget, root:
|
|
1953
|
+
return { agentsTarget, root: path6.resolve(path6.dirname(agentsTarget)) };
|
|
1014
1954
|
}
|
|
1015
1955
|
toPosix(p) {
|
|
1016
|
-
return p.split(
|
|
1956
|
+
return p.split(path6.sep).join("/");
|
|
1017
1957
|
}
|
|
1018
1958
|
canonicalRelAgent(agentFile) {
|
|
1019
1959
|
return `.agents/agents/${agentFile}`;
|
|
1020
1960
|
}
|
|
1021
1961
|
lockRel(subdir, file) {
|
|
1022
|
-
return
|
|
1962
|
+
return path6.join(subdir, file);
|
|
1023
1963
|
}
|
|
1024
1964
|
destForProjection(host, root, file) {
|
|
1025
1965
|
const def = HOST_REGISTRY[host];
|
|
1026
1966
|
const base = resolveHostProjectDir(host, root);
|
|
1027
|
-
if (def.agentsSubdir) return
|
|
1967
|
+
if (def.agentsSubdir) return path6.join(base, def.agentsSubdir, file);
|
|
1028
1968
|
return base;
|
|
1029
1969
|
}
|
|
1030
1970
|
recordProjectedTo(lockfile, relKey, projPath) {
|
|
1031
1971
|
const normProj = this.toPosix(projPath);
|
|
1032
1972
|
const posixKey = relKey.replace(/\\/g, "/");
|
|
1033
|
-
const sysKey = relKey.replace(/\//g,
|
|
1973
|
+
const sysKey = relKey.replace(/\//g, path6.sep);
|
|
1034
1974
|
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : relKey;
|
|
1035
1975
|
if (!lockfile.files[targetKey]) {
|
|
1036
1976
|
lockfile.files[targetKey] = { hash: "", installedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -1044,7 +1984,7 @@ var InstallEngine = class _InstallEngine {
|
|
|
1044
1984
|
removeProjectedTo(lockfile, canonicalRelPath, projectedRelPath) {
|
|
1045
1985
|
const normProj = this.toPosix(projectedRelPath);
|
|
1046
1986
|
const posixKey = canonicalRelPath.replace(/\\/g, "/");
|
|
1047
|
-
const sysKey = canonicalRelPath.replace(/\//g,
|
|
1987
|
+
const sysKey = canonicalRelPath.replace(/\//g, path6.sep);
|
|
1048
1988
|
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : canonicalRelPath;
|
|
1049
1989
|
const asset = lockfile.files[targetKey];
|
|
1050
1990
|
if (asset && asset.projectedTo) {
|
|
@@ -1054,23 +1994,303 @@ var InstallEngine = class _InstallEngine {
|
|
|
1054
1994
|
}
|
|
1055
1995
|
}
|
|
1056
1996
|
}
|
|
1997
|
+
/**
|
|
1998
|
+
* Plan 015c / §5.7 — host-scoped projection namespaces.
|
|
1999
|
+
*
|
|
2000
|
+
* A re-projection run is authoritative only for the paths it produces inside
|
|
2001
|
+
* its own host namespace. Bundle-scoped lanes (`.agents/plugins/<bundle>/`) are
|
|
2002
|
+
* additionally narrowed to the bundle that owns them, so re-projecting bundle A
|
|
2003
|
+
* never drops bundle B's plugin-lane entries.
|
|
2004
|
+
*/
|
|
2005
|
+
static projectionNamespacePrefixes(host, bundleName) {
|
|
2006
|
+
switch (host) {
|
|
2007
|
+
case "cline":
|
|
2008
|
+
return [".cline/", `.agents/plugins/${bundleName}/`];
|
|
2009
|
+
case "claude":
|
|
2010
|
+
return [".claude/"];
|
|
2011
|
+
case "cursor":
|
|
2012
|
+
return [".cursor/"];
|
|
2013
|
+
case "gemini":
|
|
2014
|
+
return [".gemini/"];
|
|
2015
|
+
case "opencode":
|
|
2016
|
+
return [".opencode/"];
|
|
2017
|
+
case "codex":
|
|
2018
|
+
return ["AGENTS.md"];
|
|
2019
|
+
default:
|
|
2020
|
+
return [];
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Plan 015c / §5.7 — reconcile `projectedTo` instead of only appending.
|
|
2025
|
+
*
|
|
2026
|
+
* `recordProjectedTo()` appends and is never told when a projection *path*
|
|
2027
|
+
* changes (e.g. the ADR 0016 workflow slug rename from
|
|
2028
|
+
* `.cline/workflows/implement-feature-or-fix.md` to
|
|
2029
|
+
* `.cline/workflows/workflow-implement.md`). The obsolete-projection cleanup
|
|
2030
|
+
* iterates `lockfile.projections`, which no longer contains the legacy path, so
|
|
2031
|
+
* the stale pointer in `files[canonical].projectedTo` survived forever and
|
|
2032
|
+
* `agents doctor` warned "Missing projection ... Re-run: agents add ... --fanout"
|
|
2033
|
+
* — a remediation that provably cannot clear it.
|
|
2034
|
+
*
|
|
2035
|
+
* For every canonical this run actually produced projections for, drop the
|
|
2036
|
+
* entries inside this run's namespace that the current plan did not produce.
|
|
2037
|
+
* Entries for other hosts and for other bundles survive untouched.
|
|
2038
|
+
*/
|
|
2039
|
+
reconcileProjectedTo(lockfile, host, bundleName, producedByCanonical) {
|
|
2040
|
+
const prefixes = _InstallEngine.projectionNamespacePrefixes(host, bundleName);
|
|
2041
|
+
if (prefixes.length === 0 || producedByCanonical.size === 0) return;
|
|
2042
|
+
for (const [canonical, producedPaths] of producedByCanonical) {
|
|
2043
|
+
const posixKey = canonical.replace(/\\/g, "/");
|
|
2044
|
+
const sysKey = canonical.replace(/\//g, path6.sep);
|
|
2045
|
+
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : null;
|
|
2046
|
+
if (!targetKey) continue;
|
|
2047
|
+
const asset = lockfile.files[targetKey];
|
|
2048
|
+
if (!asset.projectedTo || asset.projectedTo.length === 0) continue;
|
|
2049
|
+
const produced = new Set(producedPaths.map((p) => this.toPosix(p)));
|
|
2050
|
+
const kept = asset.projectedTo.filter(
|
|
2051
|
+
(p) => !(prefixes.some((prefix) => p.startsWith(prefix)) && !produced.has(p))
|
|
2052
|
+
);
|
|
2053
|
+
if (kept.length === asset.projectedTo.length) continue;
|
|
2054
|
+
if (kept.length === 0) {
|
|
2055
|
+
delete asset.projectedTo;
|
|
2056
|
+
} else {
|
|
2057
|
+
asset.projectedTo = kept;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* ADR 0018 — the shared compound-lane body used by every host that owns a
|
|
2063
|
+
* dedicated projector (Cline per ADR 0013, Claude per ADR 0018).
|
|
2064
|
+
*
|
|
2065
|
+
* Parameterising by `host` instead of forking the body is deliberate: the two
|
|
2066
|
+
* lanes must stay behaviourally identical in pruning, refcounting and
|
|
2067
|
+
* reconcile semantics, and the Cline path is provably unchanged because it
|
|
2068
|
+
* passes the exact same literal `'cline'` (and the same `declared` set, plan
|
|
2069
|
+
* artifacts and force flag) it previously used inline.
|
|
2070
|
+
*
|
|
2071
|
+
* The host-specific plan (the projector call) stays at the call site; this body
|
|
2072
|
+
* is purely mechanical: prune superseded projections, deploy artifacts, record
|
|
2073
|
+
* refcounted projection entries, reconcile `projectedTo`, surface `projections`.
|
|
2074
|
+
*/
|
|
2075
|
+
async applyCompoundLane(host, bundleName, artifacts, context) {
|
|
2076
|
+
const { root, lockfile, declared, projections, now, force } = context;
|
|
2077
|
+
const newPaths = new Set(artifacts.map((a) => a.relPath));
|
|
2078
|
+
if (lockfile.projections) {
|
|
2079
|
+
for (const [projRelPath, proj] of Object.entries(lockfile.projections)) {
|
|
2080
|
+
if (proj.host === host && !newPaths.has(projRelPath) && proj.owners.includes(bundleName)) {
|
|
2081
|
+
proj.owners = proj.owners.filter((o) => o !== bundleName);
|
|
2082
|
+
if (proj.owners.length === 0) {
|
|
2083
|
+
const absProjection = path6.join(root, projRelPath);
|
|
2084
|
+
if (await fs4.pathExists(absProjection)) {
|
|
2085
|
+
await fs4.remove(absProjection);
|
|
2086
|
+
await this.removeEmptyProjectionDirs(root, projRelPath);
|
|
2087
|
+
}
|
|
2088
|
+
delete lockfile.projections[projRelPath];
|
|
2089
|
+
}
|
|
2090
|
+
if (proj.canonical) {
|
|
2091
|
+
this.removeProjectedTo(lockfile, proj.canonical, projRelPath);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
for (const artifact of artifacts) {
|
|
2097
|
+
const dest = path6.join(root, artifact.relPath);
|
|
2098
|
+
if (await fs4.pathExists(dest) && !force) {
|
|
2099
|
+
const existing = await fs4.readFile(dest, "utf8").catch(() => null);
|
|
2100
|
+
if (existing !== null) {
|
|
2101
|
+
if (artifact.managedMarker && !HostProjector.hasManagedMarker(existing)) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
2104
|
+
);
|
|
2105
|
+
} else if (!artifact.managedMarker) {
|
|
2106
|
+
const registeredProj = lockfile.projections?.[artifact.relPath];
|
|
2107
|
+
if (!registeredProj) {
|
|
2108
|
+
throw new Error(
|
|
2109
|
+
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (artifact.content !== void 0) {
|
|
2116
|
+
await this.deployProjection(dest, artifact.content);
|
|
2117
|
+
} else if (artifact.sourceFilePath) {
|
|
2118
|
+
await fs4.ensureDir(path6.dirname(dest));
|
|
2119
|
+
await fs4.copy(artifact.sourceFilePath, dest, { overwrite: true });
|
|
2120
|
+
}
|
|
2121
|
+
const deployedHash = await this.calculateHash(dest);
|
|
2122
|
+
lockfile.projections = lockfile.projections || {};
|
|
2123
|
+
const existingProj = lockfile.projections[artifact.relPath];
|
|
2124
|
+
const declares = artifact.kind === "rule" || artifact.kind === "team-manifest" || artifact.kind === "plugin-manifest" || (artifact.canonical ? declared.has(artifact.canonical) : false);
|
|
2125
|
+
const bundleScoped = artifact.relPath.startsWith(".agents/plugins/");
|
|
2126
|
+
const canonicalOwners = !bundleScoped && artifact.canonical ? lockfile.files[artifact.canonical]?.owners ?? [] : [];
|
|
2127
|
+
const priorOwners = existingProj?.owners ?? [];
|
|
2128
|
+
const owners = Array.from(/* @__PURE__ */ new Set([
|
|
2129
|
+
...priorOwners,
|
|
2130
|
+
...canonicalOwners,
|
|
2131
|
+
...declares ? [bundleName] : []
|
|
2132
|
+
]));
|
|
2133
|
+
lockfile.projections[artifact.relPath] = {
|
|
2134
|
+
host,
|
|
2135
|
+
kind: artifact.kind,
|
|
2136
|
+
canonical: artifact.canonical,
|
|
2137
|
+
owners,
|
|
2138
|
+
hash: deployedHash,
|
|
2139
|
+
installedAt: existingProj?.installedAt ?? now,
|
|
2140
|
+
managedMarker: artifact.managedMarker
|
|
2141
|
+
};
|
|
2142
|
+
projections.push({ host, path: artifact.relPath, kind: artifact.kind, warnings: [] });
|
|
2143
|
+
if (artifact.canonical && !artifact.distributionOnly) {
|
|
2144
|
+
this.recordProjectedTo(lockfile, artifact.canonical, artifact.relPath);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
const producedByCanonical = /* @__PURE__ */ new Map();
|
|
2148
|
+
for (const artifact of artifacts) {
|
|
2149
|
+
if (!artifact.canonical || artifact.distributionOnly) continue;
|
|
2150
|
+
const producedPaths = producedByCanonical.get(artifact.canonical);
|
|
2151
|
+
if (producedPaths) {
|
|
2152
|
+
producedPaths.push(artifact.relPath);
|
|
2153
|
+
} else {
|
|
2154
|
+
producedByCanonical.set(artifact.canonical, [artifact.relPath]);
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
this.reconcileProjectedTo(lockfile, host, bundleName, producedByCanonical);
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* Plan 016 / ADR 0018 decision 12 — resolve the effective plugin-lane opt-in.
|
|
2161
|
+
*
|
|
2162
|
+
* An explicit `InstallOptions.pluginLane` always wins (the CLI passes `true` for `--plugin` and
|
|
2163
|
+
* `false` for `--no-plugin`); an unset flag inherits the recorded choice so that `agents update`
|
|
2164
|
+
* — which re-runs the installer without the flag — cannot silently prune an opted-in package.
|
|
2165
|
+
*/
|
|
2166
|
+
static effectivePluginLane(options, lockfile) {
|
|
2167
|
+
if (typeof options.pluginLane === "boolean") return options.pluginLane;
|
|
2168
|
+
return lockfile?.pluginLane === true;
|
|
2169
|
+
}
|
|
2170
|
+
/**
|
|
2171
|
+
* ADR 0018 — the host-specific projection plan, the single input that differs per host once
|
|
2172
|
+
* `applyCompoundLane` shared the mechanics. `pluginLane` (Plan 016 decision 13) is honoured for
|
|
2173
|
+
* the **Claude lane only**: the Cline package must stay byte-identical with the flag on and off,
|
|
2174
|
+
* so the flag is inert for `cline`.
|
|
2175
|
+
*/
|
|
2176
|
+
static async planCompoundArtifacts(host, bundleDef, scope, resolved, registryDir, pluginLane) {
|
|
2177
|
+
if (host === "cline") {
|
|
2178
|
+
return ClineProjector.planCompoundProjection(bundleDef, scope, resolved, registryDir);
|
|
2179
|
+
}
|
|
2180
|
+
const artifacts = await ClaudeProjector.planCompoundProjection(bundleDef, scope, resolved, registryDir);
|
|
2181
|
+
if (pluginLane) {
|
|
2182
|
+
artifacts.push(...await ClaudeProjector.planPluginLane(bundleDef, resolved, registryDir));
|
|
2183
|
+
}
|
|
2184
|
+
return artifacts;
|
|
2185
|
+
}
|
|
1057
2186
|
async removeEmptyProjectionDirs(workspaceRoot, projPath) {
|
|
1058
|
-
let dir =
|
|
2187
|
+
let dir = path6.dirname(path6.join(workspaceRoot, projPath));
|
|
1059
2188
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
1060
2189
|
try {
|
|
1061
|
-
const entries = await
|
|
2190
|
+
const entries = await fs4.readdir(dir);
|
|
1062
2191
|
if (entries.length > 0) break;
|
|
1063
|
-
await
|
|
2192
|
+
await fs4.rmdir(dir);
|
|
1064
2193
|
} catch {
|
|
1065
2194
|
break;
|
|
1066
2195
|
}
|
|
1067
|
-
dir =
|
|
2196
|
+
dir = path6.dirname(dir);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
// ADR 0016: Silent auto-migration of legacy .agents/workflows/*.md into
|
|
2200
|
+
// .agents/skills/workflow-<name>/SKILL.md, synchronizing lockfiles and pruning empty dirs.
|
|
2201
|
+
async migrateLegacyWorkspaceWorkflows(targetDir, lockfile) {
|
|
2202
|
+
const subPaths = AgentHostAdapter.getSubPaths(targetDir);
|
|
2203
|
+
let migratedCount = 0;
|
|
2204
|
+
let targetLockfile = lockfile;
|
|
2205
|
+
let lockfileLoadedFromDisk = false;
|
|
2206
|
+
if (!targetLockfile && await fs4.pathExists(subPaths.lockfile)) {
|
|
2207
|
+
try {
|
|
2208
|
+
targetLockfile = await fs4.readJson(subPaths.lockfile);
|
|
2209
|
+
lockfileLoadedFromDisk = true;
|
|
2210
|
+
} catch {
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (await fs4.pathExists(subPaths.workflowsDir)) {
|
|
2214
|
+
const entries = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
|
|
2215
|
+
for (const entry of entries) {
|
|
2216
|
+
if (entry.endsWith(".md")) {
|
|
2217
|
+
const legacyPath = path6.join(subPaths.workflowsDir, entry);
|
|
2218
|
+
let skillName = entry.replace(/\.md$/, "").replace(/--/g, "-");
|
|
2219
|
+
if (!skillName.startsWith("workflow-")) {
|
|
2220
|
+
skillName = `workflow-${skillName}`;
|
|
2221
|
+
}
|
|
2222
|
+
const targetSkillDir = path6.join(subPaths.skillsDir, skillName);
|
|
2223
|
+
const targetSkillFile = path6.join(targetSkillDir, "SKILL.md");
|
|
2224
|
+
if (!await fs4.pathExists(targetSkillFile)) {
|
|
2225
|
+
await fs4.ensureDir(targetSkillDir);
|
|
2226
|
+
const legacyContent = await fs4.readFile(legacyPath, "utf8");
|
|
2227
|
+
let skillContent = legacyContent;
|
|
2228
|
+
const fmMatch = legacyContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
2229
|
+
if (fmMatch) {
|
|
2230
|
+
try {
|
|
2231
|
+
const meta = YAML2.parse(fmMatch[1]) || {};
|
|
2232
|
+
meta.name = skillName;
|
|
2233
|
+
if (!meta.metadata) {
|
|
2234
|
+
meta.metadata = {
|
|
2235
|
+
author: "Agents United",
|
|
2236
|
+
version: "1.0.0",
|
|
2237
|
+
icon: "\u{1F504}"
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
const rest = legacyContent.replace(/^---\r?\n[\s\S]+?\r?\n---/, "");
|
|
2241
|
+
skillContent = `---
|
|
2242
|
+
${YAML2.stringify(meta).trim()}
|
|
2243
|
+
---${rest}`;
|
|
2244
|
+
} catch {
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
await fs4.writeFile(targetSkillFile, skillContent, "utf8");
|
|
2248
|
+
}
|
|
2249
|
+
await fs4.remove(legacyPath).catch(() => {
|
|
2250
|
+
});
|
|
2251
|
+
migratedCount++;
|
|
2252
|
+
if (targetLockfile) {
|
|
2253
|
+
const legacyRel = this.toPosix(path6.relative(targetDir, legacyPath));
|
|
2254
|
+
const newRel = this.toPosix(path6.relative(targetDir, targetSkillFile));
|
|
2255
|
+
if (targetLockfile.files && targetLockfile.files[legacyRel]) {
|
|
2256
|
+
targetLockfile.files[newRel] = targetLockfile.files[legacyRel];
|
|
2257
|
+
delete targetLockfile.files[legacyRel];
|
|
2258
|
+
}
|
|
2259
|
+
if (targetLockfile.installed && targetLockfile.installed.skills) {
|
|
2260
|
+
if (!targetLockfile.installed.skills.includes(skillName)) {
|
|
2261
|
+
targetLockfile.installed.skills.push(skillName);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const remaining = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
|
|
2268
|
+
if (remaining.length === 0) {
|
|
2269
|
+
await fs4.remove(subPaths.workflowsDir).catch(() => {
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
if (targetLockfile && targetLockfile.installed && targetLockfile.installed.workflows && targetLockfile.installed.workflows.length > 0) {
|
|
2274
|
+
for (const w of targetLockfile.installed.workflows) {
|
|
2275
|
+
let skillName = w.replace(/\.md$/, "").replace(/--/g, "-");
|
|
2276
|
+
if (!skillName.startsWith("workflow-")) {
|
|
2277
|
+
skillName = `workflow-${skillName}`;
|
|
2278
|
+
}
|
|
2279
|
+
if (targetLockfile.installed.skills && !targetLockfile.installed.skills.includes(skillName)) {
|
|
2280
|
+
targetLockfile.installed.skills.push(skillName);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
targetLockfile.installed.workflows = [];
|
|
2284
|
+
}
|
|
2285
|
+
if (targetLockfile && (lockfileLoadedFromDisk || await fs4.pathExists(subPaths.lockfile))) {
|
|
2286
|
+
await fs4.writeJson(subPaths.lockfile, targetLockfile, { spaces: 2 });
|
|
1068
2287
|
}
|
|
2288
|
+
return migratedCount;
|
|
1069
2289
|
}
|
|
1070
2290
|
/**
|
|
1071
2291
|
* Dry-run: compute the projection plan (paths + warnings) without writing anything.
|
|
1072
2292
|
*/
|
|
1073
|
-
async buildProjections(fanoutHosts, resolved, registryDir, scope, targetDir) {
|
|
2293
|
+
async buildProjections(fanoutHosts, resolved, registryDir, scope, targetDir, pluginLane = false) {
|
|
1074
2294
|
const infos = [];
|
|
1075
2295
|
if (fanoutHosts.length === 0) return infos;
|
|
1076
2296
|
const { root } = _InstallEngine.projectionRoot(scope, targetDir);
|
|
@@ -1079,10 +2299,17 @@ var InstallEngine = class _InstallEngine {
|
|
|
1079
2299
|
infos.push({ host, path: "AGENTS.md", kind: "bridge", warnings: [] });
|
|
1080
2300
|
continue;
|
|
1081
2301
|
}
|
|
1082
|
-
if (host === "cline") {
|
|
2302
|
+
if (host === "cline" || host === "claude") {
|
|
1083
2303
|
const bundleDef = resolved.targetBundle ? await this.registry.getBundle(resolved.targetBundle) : void 0;
|
|
1084
2304
|
if (bundleDef) {
|
|
1085
|
-
const artifacts = await
|
|
2305
|
+
const artifacts = await _InstallEngine.planCompoundArtifacts(
|
|
2306
|
+
host,
|
|
2307
|
+
bundleDef,
|
|
2308
|
+
scope,
|
|
2309
|
+
resolved,
|
|
2310
|
+
registryDir,
|
|
2311
|
+
pluginLane
|
|
2312
|
+
);
|
|
1086
2313
|
for (const artifact of artifacts) {
|
|
1087
2314
|
infos.push({ host, path: artifact.relPath, kind: artifact.kind, warnings: [] });
|
|
1088
2315
|
}
|
|
@@ -1092,10 +2319,10 @@ var InstallEngine = class _InstallEngine {
|
|
|
1092
2319
|
const base = resolveHostProjectDir(host, root);
|
|
1093
2320
|
const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
|
|
1094
2321
|
for (const agentFile of resolved.agents) {
|
|
1095
|
-
const content = await
|
|
2322
|
+
const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
|
|
1096
2323
|
const res = HostProjector.projectAgent(content, HOST_REGISTRY[host].profile, this.canonicalRelAgent(agentFile));
|
|
1097
|
-
const dest =
|
|
1098
|
-
infos.push({ host, path: this.toPosix(
|
|
2324
|
+
const dest = path6.join(base, subdir, agentFile);
|
|
2325
|
+
infos.push({ host, path: this.toPosix(path6.relative(root, dest)), kind: "role", warnings: res.warnings });
|
|
1099
2326
|
}
|
|
1100
2327
|
}
|
|
1101
2328
|
return infos;
|
|
@@ -1109,6 +2336,14 @@ var InstallEngine = class _InstallEngine {
|
|
|
1109
2336
|
if (fanoutHosts.length === 0) return;
|
|
1110
2337
|
const { root } = _InstallEngine.projectionRoot(scope, options.targetDir);
|
|
1111
2338
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2339
|
+
const pluginLane = _InstallEngine.effectivePluginLane(options, lockfile);
|
|
2340
|
+
if (fanoutHosts.includes("claude")) {
|
|
2341
|
+
if (pluginLane) {
|
|
2342
|
+
lockfile.pluginLane = true;
|
|
2343
|
+
} else if (options.pluginLane === false) {
|
|
2344
|
+
delete lockfile.pluginLane;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
1112
2347
|
for (const host of fanoutHosts) {
|
|
1113
2348
|
if (host === "codex") {
|
|
1114
2349
|
const assets = [];
|
|
@@ -1116,9 +2351,9 @@ var InstallEngine = class _InstallEngine {
|
|
|
1116
2351
|
resolved.workflows.forEach((f) => assets.push({ name: f.replace(/\.md$/, ""), type: "workflow", relPath: `.agents/workflows/${f}` }));
|
|
1117
2352
|
resolved.skills.forEach((s) => assets.push({ name: s, type: "skill", relPath: `.agents/skills/${s}/SKILL.md` }));
|
|
1118
2353
|
const bridge = HostProjector.buildAgentsMdIndex(assets);
|
|
1119
|
-
const bridgeDest =
|
|
1120
|
-
if (await
|
|
1121
|
-
const existing = await
|
|
2354
|
+
const bridgeDest = path6.join(root, "AGENTS.md");
|
|
2355
|
+
if (await fs4.pathExists(bridgeDest) && !options.force) {
|
|
2356
|
+
const existing = await fs4.readFile(bridgeDest, "utf8");
|
|
1122
2357
|
if (!HostProjector.hasManagedMarker(existing)) {
|
|
1123
2358
|
throw new Error(
|
|
1124
2359
|
"File AGENTS.md already exists at the workspace root and is not managed by agents-united. Use --force to overwrite."
|
|
@@ -1129,10 +2364,10 @@ var InstallEngine = class _InstallEngine {
|
|
|
1129
2364
|
projections.push({ host, path: "AGENTS.md", kind: "bridge", warnings: [] });
|
|
1130
2365
|
resolved.agents.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("agents", f), "AGENTS.md"));
|
|
1131
2366
|
resolved.workflows.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("workflows", f), "AGENTS.md"));
|
|
1132
|
-
resolved.skills.forEach((s) => this.recordProjectedTo(lockfile, this.lockRel("skills",
|
|
2367
|
+
resolved.skills.forEach((s) => this.recordProjectedTo(lockfile, this.lockRel("skills", path6.join(s, "SKILL.md")), "AGENTS.md"));
|
|
1133
2368
|
continue;
|
|
1134
2369
|
}
|
|
1135
|
-
if (host === "cline") {
|
|
2370
|
+
if (host === "cline" || host === "claude") {
|
|
1136
2371
|
const bundleDef = resolved.targetBundle ? await this.registry.getBundle(resolved.targetBundle) : void 0;
|
|
1137
2372
|
if (bundleDef) {
|
|
1138
2373
|
const declared = /* @__PURE__ */ new Set();
|
|
@@ -1140,73 +2375,23 @@ var InstallEngine = class _InstallEngine {
|
|
|
1140
2375
|
(bundleDef.agents || []).forEach((a) => declared.add(`agents/${a}`));
|
|
1141
2376
|
(bundleDef.skills || []).forEach((s) => declared.add(`skills/${s}/SKILL.md`));
|
|
1142
2377
|
(bundleDef.workflows || []).forEach((w) => declared.add(`workflows/${w}`));
|
|
1143
|
-
const artifacts = await
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
for (const artifact of artifacts) {
|
|
1164
|
-
const dest = path5.join(root, artifact.relPath);
|
|
1165
|
-
if (await fs3.pathExists(dest) && !options.force) {
|
|
1166
|
-
const existing = await fs3.readFile(dest, "utf8").catch(() => null);
|
|
1167
|
-
if (existing !== null) {
|
|
1168
|
-
if (artifact.managedMarker && !HostProjector.hasManagedMarker(existing)) {
|
|
1169
|
-
throw new Error(
|
|
1170
|
-
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
1171
|
-
);
|
|
1172
|
-
} else if (!artifact.managedMarker) {
|
|
1173
|
-
const registeredProj = lockfile.projections?.[artifact.relPath];
|
|
1174
|
-
if (!registeredProj) {
|
|
1175
|
-
throw new Error(
|
|
1176
|
-
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
1177
|
-
);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
if (artifact.content !== void 0) {
|
|
1183
|
-
await this.deployProjection(dest, artifact.content);
|
|
1184
|
-
} else if (artifact.sourceFilePath) {
|
|
1185
|
-
await fs3.ensureDir(path5.dirname(dest));
|
|
1186
|
-
await fs3.copy(artifact.sourceFilePath, dest, { overwrite: true });
|
|
1187
|
-
}
|
|
1188
|
-
const deployedHash = await this.calculateHash(dest);
|
|
1189
|
-
lockfile.projections = lockfile.projections || {};
|
|
1190
|
-
const existingProj = lockfile.projections[artifact.relPath];
|
|
1191
|
-
const bundleName = bundleDef.name;
|
|
1192
|
-
const declares = artifact.kind === "rule" || artifact.kind === "team-manifest" || artifact.kind === "plugin-manifest" || (artifact.canonical ? declared.has(artifact.canonical) : false);
|
|
1193
|
-
const priorOwners = existingProj?.owners ?? [];
|
|
1194
|
-
const owners = declares ? Array.from(/* @__PURE__ */ new Set([...priorOwners, bundleName])) : priorOwners;
|
|
1195
|
-
lockfile.projections[artifact.relPath] = {
|
|
1196
|
-
host: "cline",
|
|
1197
|
-
kind: artifact.kind,
|
|
1198
|
-
canonical: artifact.canonical,
|
|
1199
|
-
owners,
|
|
1200
|
-
hash: deployedHash,
|
|
1201
|
-
installedAt: existingProj?.installedAt ?? now,
|
|
1202
|
-
managedMarker: artifact.managedMarker
|
|
1203
|
-
};
|
|
1204
|
-
projections.push({ host, path: artifact.relPath, kind: artifact.kind, warnings: [] });
|
|
1205
|
-
if (artifact.canonical) {
|
|
1206
|
-
this.recordProjectedTo(lockfile, artifact.canonical, artifact.relPath);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
if (bundleDef.parentBundle) {
|
|
2378
|
+
const artifacts = await _InstallEngine.planCompoundArtifacts(
|
|
2379
|
+
host,
|
|
2380
|
+
bundleDef,
|
|
2381
|
+
scope,
|
|
2382
|
+
resolved,
|
|
2383
|
+
registryDir,
|
|
2384
|
+
pluginLane
|
|
2385
|
+
);
|
|
2386
|
+
await this.applyCompoundLane(host, bundleDef.name, artifacts, {
|
|
2387
|
+
root,
|
|
2388
|
+
lockfile,
|
|
2389
|
+
declared,
|
|
2390
|
+
projections,
|
|
2391
|
+
now,
|
|
2392
|
+
force: options.force
|
|
2393
|
+
});
|
|
2394
|
+
if (host === "cline" && bundleDef.parentBundle) {
|
|
1210
2395
|
const parentDef = await this.registry.getBundle(bundleDef.parentBundle);
|
|
1211
2396
|
if (parentDef) {
|
|
1212
2397
|
const parentProjection = await ClineProjector.planCompoundProjection(
|
|
@@ -1219,8 +2404,8 @@ var InstallEngine = class _InstallEngine {
|
|
|
1219
2404
|
for (const artifact of parentProjection) {
|
|
1220
2405
|
if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
|
|
1221
2406
|
if (artifact.content === void 0) continue;
|
|
1222
|
-
const dest =
|
|
1223
|
-
if (!await
|
|
2407
|
+
const dest = path6.join(root, artifact.relPath);
|
|
2408
|
+
if (!await fs4.pathExists(dest)) continue;
|
|
1224
2409
|
await this.deployProjection(dest, artifact.content);
|
|
1225
2410
|
const deployedHash = await this.calculateHash(dest);
|
|
1226
2411
|
lockfile.projections = lockfile.projections || {};
|
|
@@ -1244,23 +2429,54 @@ var InstallEngine = class _InstallEngine {
|
|
|
1244
2429
|
const base = resolveHostProjectDir(host, root);
|
|
1245
2430
|
const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
|
|
1246
2431
|
for (const agentFile of resolved.agents) {
|
|
1247
|
-
const content = await
|
|
2432
|
+
const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
|
|
1248
2433
|
const canonicalRel = this.canonicalRelAgent(agentFile);
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
const
|
|
1252
|
-
|
|
1253
|
-
|
|
2434
|
+
const isCoordinatorRole = /^\s*type:\s*orchestrator\s*$/m.test(content) || /^\s*mainAgent:\s*true\s*$/m.test(content);
|
|
2435
|
+
const peerRoles = resolved.agents.filter((f) => f !== agentFile).map((f) => ClaudeProjector.stripSubagentPrefix(f.replace(/\.md$/i, "")));
|
|
2436
|
+
const res = host === "claude" ? ClaudeProjector.renderRole(content, canonicalRel, {
|
|
2437
|
+
allowlist: isCoordinatorRole ? peerRoles : void 0
|
|
2438
|
+
}) : HostProjector.projectAgent(content, HOST_REGISTRY[host].profile, canonicalRel);
|
|
2439
|
+
const strippedRoleName = agentFile.replace(/\.md$/i, "").replace(/^subagent-/, "");
|
|
2440
|
+
const projName = host === "cline" ? `${strippedRoleName}.yml` : host === "claude" ? `${strippedRoleName}.md` : agentFile;
|
|
2441
|
+
const dest = path6.join(base, subdir, projName);
|
|
2442
|
+
const projPath = this.toPosix(path6.relative(root, dest));
|
|
2443
|
+
const existingRoleProj = lockfile.projections?.[projPath];
|
|
2444
|
+
if (existingRoleProj?.kind === "role") {
|
|
2445
|
+
const compoundOwned = !(existingRoleProj.canonical ?? "").startsWith(".agents/");
|
|
2446
|
+
if (host === "cline" || host === "claude" && compoundOwned) {
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
if (await fs4.pathExists(dest) && !options.force) {
|
|
2451
|
+
const existing = await fs4.readFile(dest, "utf8");
|
|
1254
2452
|
if (!HostProjector.hasManagedMarker(existing)) {
|
|
1255
2453
|
throw new Error(
|
|
1256
|
-
`Projection target ${this.toPosix(
|
|
2454
|
+
`Projection target ${this.toPosix(path6.relative(root, dest))} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
1257
2455
|
);
|
|
1258
2456
|
}
|
|
1259
2457
|
}
|
|
1260
2458
|
await this.deployProjection(dest, res.content);
|
|
1261
|
-
|
|
1262
|
-
|
|
2459
|
+
projections.push({
|
|
2460
|
+
host,
|
|
2461
|
+
path: projPath,
|
|
2462
|
+
kind: "role",
|
|
2463
|
+
warnings: "warnings" in res ? res.warnings : []
|
|
2464
|
+
});
|
|
1263
2465
|
this.recordProjectedTo(lockfile, this.lockRel("agents", agentFile), projPath);
|
|
2466
|
+
if (host === "cline" || host === "claude") {
|
|
2467
|
+
lockfile.projections = lockfile.projections || {};
|
|
2468
|
+
const existingProj = lockfile.projections[projPath];
|
|
2469
|
+
const canonicalRecord = lockfile.files[canonicalRel] ?? lockfile.files[canonicalRel.replace(/^\.agents\//, "")];
|
|
2470
|
+
lockfile.projections[projPath] = {
|
|
2471
|
+
host,
|
|
2472
|
+
kind: "role",
|
|
2473
|
+
canonical: canonicalRel,
|
|
2474
|
+
owners: mergeAssetOwners(existingProj, resolved.targetBundle ?? void 0).concat(canonicalRecord?.owners ?? []).filter((o, i, arr) => arr.indexOf(o) === i),
|
|
2475
|
+
hash: await this.calculateHash(dest),
|
|
2476
|
+
installedAt: existingProj?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2477
|
+
managedMarker: true
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
1264
2480
|
}
|
|
1265
2481
|
}
|
|
1266
2482
|
}
|
|
@@ -1275,17 +2491,19 @@ var InstallEngine = class _InstallEngine {
|
|
|
1275
2491
|
const fanoutHosts = (options.fanout || []).map((h) => typeof h === "string" ? h.trim().toLowerCase() : h).filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
|
|
1276
2492
|
if (options.dryRun) {
|
|
1277
2493
|
let effectiveDryFanout = fanoutHosts;
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
2494
|
+
let effectiveDryPluginLane = options.pluginLane === true;
|
|
2495
|
+
const agentsTarget2 = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
|
|
2496
|
+
const lockfilePath = path6.join(agentsTarget2, "agents-united.json");
|
|
2497
|
+
if (await fs4.pathExists(lockfilePath)) {
|
|
2498
|
+
const lockfile = await fs4.readJson(lockfilePath).catch(() => null);
|
|
2499
|
+
if (options.fanout === void 0 && hasCanonicalAgents && lockfile?.fanout) {
|
|
2500
|
+
effectiveDryFanout = lockfile.fanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
|
|
2501
|
+
}
|
|
2502
|
+
if (options.pluginLane === void 0 && lockfile?.pluginLane === true) {
|
|
2503
|
+
effectiveDryPluginLane = true;
|
|
1286
2504
|
}
|
|
1287
2505
|
}
|
|
1288
|
-
const projections2 = hasCanonicalAgents ? await this.buildProjections(effectiveDryFanout, resolved, registryDir, scope, options.targetDir) : [];
|
|
2506
|
+
const projections2 = hasCanonicalAgents ? await this.buildProjections(effectiveDryFanout, resolved, registryDir, scope, options.targetDir, effectiveDryPluginLane) : [];
|
|
1289
2507
|
return { installed: resolved, targetDirs, dryRun: true, method, projections: projections2 };
|
|
1290
2508
|
}
|
|
1291
2509
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1293,11 +2511,14 @@ var InstallEngine = class _InstallEngine {
|
|
|
1293
2511
|
const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
|
|
1294
2512
|
for (const targetDir of targetDirs) {
|
|
1295
2513
|
const subPaths = AgentHostAdapter.getSubPaths(targetDir);
|
|
1296
|
-
await
|
|
1297
|
-
await
|
|
1298
|
-
|
|
1299
|
-
|
|
2514
|
+
await fs4.ensureDir(subPaths.agentsDir);
|
|
2515
|
+
await fs4.ensureDir(subPaths.skillsDir);
|
|
2516
|
+
if (resolved.workflows && resolved.workflows.length > 0) {
|
|
2517
|
+
await fs4.ensureDir(subPaths.workflowsDir);
|
|
2518
|
+
}
|
|
2519
|
+
await fs4.ensureDir(subPaths.rulesDir);
|
|
1300
2520
|
const lockfile = await this.readLockfile(subPaths.lockfile);
|
|
2521
|
+
await this.migrateLegacyWorkspaceWorkflows(targetDir, lockfile);
|
|
1301
2522
|
lockfile.scope = scope;
|
|
1302
2523
|
lockfile.method = method;
|
|
1303
2524
|
lockfile.hosts = hosts;
|
|
@@ -1306,32 +2527,34 @@ var InstallEngine = class _InstallEngine {
|
|
|
1306
2527
|
lockfile.fanout = effectiveFanout;
|
|
1307
2528
|
}
|
|
1308
2529
|
const declared = /* @__PURE__ */ new Set();
|
|
2530
|
+
let hasDeclaredRoster = false;
|
|
1309
2531
|
if (resolved.targetBundle) {
|
|
1310
2532
|
const bundleDef = await this.registry.getBundle(resolved.targetBundle);
|
|
1311
2533
|
if (bundleDef) {
|
|
2534
|
+
hasDeclaredRoster = true;
|
|
1312
2535
|
if (bundleDef.orchestrator) declared.add(`agents/${bundleDef.orchestrator}`);
|
|
1313
2536
|
(bundleDef.agents || []).forEach((a) => declared.add(`agents/${a}`));
|
|
1314
2537
|
(bundleDef.skills || []).forEach((s) => declared.add(`skills/${s}/SKILL.md`));
|
|
1315
2538
|
(bundleDef.workflows || []).forEach((w) => declared.add(`workflows/${w}`));
|
|
1316
2539
|
}
|
|
1317
2540
|
}
|
|
2541
|
+
const declaresAsset = (assetKey) => !hasDeclaredRoster || declared.has(assetKey);
|
|
1318
2542
|
for (const agentFile of resolved.agents) {
|
|
1319
|
-
const src =
|
|
1320
|
-
const dest =
|
|
1321
|
-
if (await
|
|
2543
|
+
const src = path6.join(registryDir, "agents", agentFile);
|
|
2544
|
+
const dest = path6.join(subPaths.agentsDir, agentFile);
|
|
2545
|
+
if (await fs4.pathExists(dest) && !options.force && method !== "symlink") {
|
|
1322
2546
|
const existingHash = await this.calculateHash(dest).catch(() => null);
|
|
1323
|
-
const relPath2 =
|
|
2547
|
+
const relPath2 = path6.relative(targetDir, dest);
|
|
1324
2548
|
if (existingHash && lockfile.files[relPath2] && lockfile.files[relPath2].hash !== existingHash) {
|
|
1325
2549
|
throw new Error(`File ${relPath2} has user modifications. Use --force to overwrite.`);
|
|
1326
2550
|
}
|
|
1327
2551
|
}
|
|
1328
2552
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1329
2553
|
const hash = await this.calculateHash(src);
|
|
1330
|
-
const relPath = this.toPosix(
|
|
2554
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1331
2555
|
const existing = lockfile.files[relPath];
|
|
1332
|
-
const existingOwners = existing?.owners ??
|
|
1333
|
-
const
|
|
1334
|
-
const owners = resolved.targetBundle && declaresAsset ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
2556
|
+
const existingOwners = existing?.owners ?? [];
|
|
2557
|
+
const owners = resolved.targetBundle && declaresAsset(`agents/${agentFile}`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
1335
2558
|
lockfile.files[relPath] = {
|
|
1336
2559
|
hash,
|
|
1337
2560
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1345,17 +2568,16 @@ var InstallEngine = class _InstallEngine {
|
|
|
1345
2568
|
}
|
|
1346
2569
|
}
|
|
1347
2570
|
for (const skillName of resolved.skills) {
|
|
1348
|
-
const src =
|
|
1349
|
-
const dest =
|
|
2571
|
+
const src = path6.join(registryDir, "skills", skillName);
|
|
2572
|
+
const dest = path6.join(subPaths.skillsDir, skillName);
|
|
1350
2573
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1351
|
-
const skillFile =
|
|
1352
|
-
if (await
|
|
2574
|
+
const skillFile = path6.join(src, "SKILL.md");
|
|
2575
|
+
if (await fs4.pathExists(skillFile)) {
|
|
1353
2576
|
const hash = await this.calculateHash(skillFile);
|
|
1354
|
-
const relPath = this.toPosix(
|
|
2577
|
+
const relPath = this.toPosix(path6.relative(targetDir, path6.join(subPaths.skillsDir, skillName, "SKILL.md")));
|
|
1355
2578
|
const existing = lockfile.files[relPath];
|
|
1356
|
-
const existingOwners = existing?.owners ??
|
|
1357
|
-
const
|
|
1358
|
-
const owners = resolved.targetBundle && declaresAsset ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
2579
|
+
const existingOwners = existing?.owners ?? [];
|
|
2580
|
+
const owners = resolved.targetBundle && declaresAsset(`skills/${skillName}/SKILL.md`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
1359
2581
|
lockfile.files[relPath] = {
|
|
1360
2582
|
hash,
|
|
1361
2583
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1365,20 +2587,43 @@ var InstallEngine = class _InstallEngine {
|
|
|
1365
2587
|
...existing?.projectedTo ? { projectedTo: existing.projectedTo } : {}
|
|
1366
2588
|
};
|
|
1367
2589
|
}
|
|
2590
|
+
const sidecarStack = [dest];
|
|
2591
|
+
while (sidecarStack.length > 0) {
|
|
2592
|
+
const dir = sidecarStack.pop();
|
|
2593
|
+
for (const entry of await fs4.readdir(dir, { withFileTypes: true })) {
|
|
2594
|
+
const abs = path6.join(dir, entry.name);
|
|
2595
|
+
if (entry.isDirectory()) {
|
|
2596
|
+
sidecarStack.push(abs);
|
|
2597
|
+
continue;
|
|
2598
|
+
}
|
|
2599
|
+
const sideRel = this.toPosix(path6.relative(targetDir, abs));
|
|
2600
|
+
if (abs === path6.join(dest, "SKILL.md")) continue;
|
|
2601
|
+
const existingSide = lockfile.files[sideRel];
|
|
2602
|
+
const existingSideOwners = existingSide?.owners ?? [];
|
|
2603
|
+
const sideOwners = resolved.targetBundle && declaresAsset(`skills/${skillName}/SKILL.md`) ? Array.from(/* @__PURE__ */ new Set([...existingSideOwners, resolved.targetBundle])) : existingSideOwners;
|
|
2604
|
+
lockfile.files[sideRel] = {
|
|
2605
|
+
hash: await this.calculateHash(abs),
|
|
2606
|
+
bundle: existingSide?.bundle ?? resolved.targetBundle,
|
|
2607
|
+
owners: sideOwners,
|
|
2608
|
+
method: actualMethod,
|
|
2609
|
+
installedAt: existingSide?.installedAt ?? now,
|
|
2610
|
+
...existingSide?.projectedTo ? { projectedTo: existingSide.projectedTo } : {}
|
|
2611
|
+
};
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
1368
2614
|
if (!lockfile.installed.skills.includes(skillName)) {
|
|
1369
2615
|
lockfile.installed.skills.push(skillName);
|
|
1370
2616
|
}
|
|
1371
2617
|
}
|
|
1372
2618
|
for (const workflowFile of resolved.workflows) {
|
|
1373
|
-
const src =
|
|
1374
|
-
const dest =
|
|
2619
|
+
const src = path6.join(registryDir, "workflows", workflowFile);
|
|
2620
|
+
const dest = path6.join(subPaths.workflowsDir, workflowFile);
|
|
1375
2621
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1376
2622
|
const hash = await this.calculateHash(src);
|
|
1377
|
-
const relPath = this.toPosix(
|
|
2623
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1378
2624
|
const existing = lockfile.files[relPath];
|
|
1379
|
-
const existingOwners = existing?.owners ??
|
|
1380
|
-
const
|
|
1381
|
-
const owners = resolved.targetBundle && declaresAsset ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
2625
|
+
const existingOwners = existing?.owners ?? [];
|
|
2626
|
+
const owners = resolved.targetBundle && declaresAsset(`workflows/${workflowFile}`) ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
1382
2627
|
lockfile.files[relPath] = {
|
|
1383
2628
|
hash,
|
|
1384
2629
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1387,21 +2632,21 @@ var InstallEngine = class _InstallEngine {
|
|
|
1387
2632
|
installedAt: existing?.installedAt ?? now,
|
|
1388
2633
|
...existing?.projectedTo ? { projectedTo: existing.projectedTo } : {}
|
|
1389
2634
|
};
|
|
2635
|
+
lockfile.installed.workflows = lockfile.installed.workflows || [];
|
|
1390
2636
|
if (!lockfile.installed.workflows.includes(workflowFile)) {
|
|
1391
2637
|
lockfile.installed.workflows.push(workflowFile);
|
|
1392
2638
|
}
|
|
1393
2639
|
}
|
|
1394
2640
|
for (const ruleFile of resolved.rules) {
|
|
1395
|
-
const src =
|
|
1396
|
-
const dest =
|
|
1397
|
-
if (await
|
|
2641
|
+
const src = path6.join(registryDir, "rules", ruleFile);
|
|
2642
|
+
const dest = path6.join(subPaths.rulesDir, ruleFile);
|
|
2643
|
+
if (await fs4.pathExists(src)) {
|
|
1398
2644
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1399
2645
|
const hash = await this.calculateHash(src);
|
|
1400
|
-
const relPath = this.toPosix(
|
|
2646
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1401
2647
|
const existing = lockfile.files[relPath];
|
|
1402
|
-
const existingOwners = existing?.owners ??
|
|
1403
|
-
const
|
|
1404
|
-
const owners = resolved.targetBundle && declaresAsset ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
2648
|
+
const existingOwners = existing?.owners ?? [];
|
|
2649
|
+
const owners = resolved.targetBundle ? Array.from(/* @__PURE__ */ new Set([...existingOwners, resolved.targetBundle])) : existingOwners;
|
|
1405
2650
|
lockfile.files[relPath] = {
|
|
1406
2651
|
hash,
|
|
1407
2652
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1424,26 +2669,26 @@ var InstallEngine = class _InstallEngine {
|
|
|
1424
2669
|
lockfile.bundleModes[resolved.targetBundle] = options.mode;
|
|
1425
2670
|
}
|
|
1426
2671
|
}
|
|
1427
|
-
if (hasCanonicalAgents && effectiveFanout.length > 0 &&
|
|
2672
|
+
if (hasCanonicalAgents && effectiveFanout.length > 0 && path6.resolve(targetDir) === path6.resolve(agentsTarget)) {
|
|
1428
2673
|
await this.applyFanout(effectiveFanout, resolved, registryDir, scope, targetDir, lockfile, options, projections);
|
|
1429
2674
|
}
|
|
1430
|
-
await
|
|
2675
|
+
await fs4.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
1431
2676
|
}
|
|
1432
2677
|
return { installed: resolved, targetDirs, dryRun: false, method, projections };
|
|
1433
2678
|
}
|
|
1434
2679
|
};
|
|
1435
2680
|
|
|
1436
2681
|
// src/core/uninstaller.ts
|
|
1437
|
-
import
|
|
2682
|
+
import path7 from "path";
|
|
1438
2683
|
import crypto2 from "crypto";
|
|
1439
|
-
import
|
|
2684
|
+
import fs5 from "fs-extra";
|
|
1440
2685
|
var UninstallEngine = class {
|
|
1441
2686
|
registry;
|
|
1442
2687
|
constructor(registry2) {
|
|
1443
2688
|
this.registry = registry2 || new RegistryResolver();
|
|
1444
2689
|
}
|
|
1445
2690
|
async calculateHash(filePath) {
|
|
1446
|
-
const buffer = await
|
|
2691
|
+
const buffer = await fs5.readFile(filePath);
|
|
1447
2692
|
return "sha256:" + crypto2.createHash("sha256").update(buffer).digest("hex");
|
|
1448
2693
|
}
|
|
1449
2694
|
/**
|
|
@@ -1452,42 +2697,47 @@ var UninstallEngine = class {
|
|
|
1452
2697
|
* AGENTS.md bridge (agentsmd profile) is a plain markdown index with no
|
|
1453
2698
|
* frontmatter, so the marker is checked against the whole content.
|
|
1454
2699
|
*/
|
|
1455
|
-
async isManagedProjection(absPath, isAgentsMd) {
|
|
1456
|
-
const content = await
|
|
2700
|
+
async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
2701
|
+
const content = await fs5.readFile(absPath, "utf8");
|
|
1457
2702
|
if (isAgentsMd) {
|
|
1458
2703
|
return content.includes("managed-by: agents-united");
|
|
1459
2704
|
}
|
|
1460
|
-
|
|
2705
|
+
if (HostProjector.hasManagedMarker(content)) return true;
|
|
2706
|
+
if (recordedHash) {
|
|
2707
|
+
const bytes = await fs5.readFile(absPath);
|
|
2708
|
+
return `sha256:${crypto2.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
|
|
2709
|
+
}
|
|
2710
|
+
return false;
|
|
1461
2711
|
}
|
|
1462
2712
|
/**
|
|
1463
2713
|
* Removes empty projection dirs left behind after deleting a projected file
|
|
1464
2714
|
* (e.g. `.claude/agents/` and then `.claude/`). Only ever removes empty dirs.
|
|
1465
2715
|
*/
|
|
1466
2716
|
async removeEmptyProjectionDirs(workspaceRoot, projPath) {
|
|
1467
|
-
let dir =
|
|
2717
|
+
let dir = path7.dirname(path7.join(workspaceRoot, projPath));
|
|
1468
2718
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
1469
2719
|
try {
|
|
1470
|
-
const entries = await
|
|
2720
|
+
const entries = await fs5.readdir(dir);
|
|
1471
2721
|
if (entries.length > 0) break;
|
|
1472
|
-
await
|
|
2722
|
+
await fs5.rmdir(dir);
|
|
1473
2723
|
} catch {
|
|
1474
2724
|
break;
|
|
1475
2725
|
}
|
|
1476
|
-
dir =
|
|
2726
|
+
dir = path7.dirname(dir);
|
|
1477
2727
|
}
|
|
1478
2728
|
}
|
|
1479
2729
|
/**
|
|
1480
2730
|
* Removes every recorded projection for a canonical asset, guarding against
|
|
1481
2731
|
* clobbering user-modified files. Mirrors the hash-conflict error pattern.
|
|
1482
2732
|
*/
|
|
1483
|
-
async removeProjections(workspaceRoot, projectedTo, force) {
|
|
2733
|
+
async removeProjections(workspaceRoot, projectedTo, force, projectionHashes) {
|
|
1484
2734
|
for (const projPath of projectedTo) {
|
|
1485
|
-
const absProjection =
|
|
1486
|
-
if (!await
|
|
2735
|
+
const absProjection = path7.join(workspaceRoot, projPath);
|
|
2736
|
+
if (!await fs5.pathExists(absProjection)) {
|
|
1487
2737
|
continue;
|
|
1488
2738
|
}
|
|
1489
2739
|
const isAgentsMd = projPath === "AGENTS.md";
|
|
1490
|
-
const managed = await this.isManagedProjection(absProjection, isAgentsMd);
|
|
2740
|
+
const managed = await this.isManagedProjection(absProjection, isAgentsMd, projectionHashes?.[projPath]?.hash);
|
|
1491
2741
|
if (!managed && !force) {
|
|
1492
2742
|
throw new Error(
|
|
1493
2743
|
`Projection ${projPath} has user modifications. Use --force to remove.`
|
|
@@ -1499,14 +2749,14 @@ var UninstallEngine = class {
|
|
|
1499
2749
|
}
|
|
1500
2750
|
async safeRemoveProjection(absPath) {
|
|
1501
2751
|
try {
|
|
1502
|
-
const lstat = await
|
|
2752
|
+
const lstat = await fs5.lstat(absPath);
|
|
1503
2753
|
if (lstat.isSymbolicLink()) {
|
|
1504
|
-
await
|
|
2754
|
+
await fs5.unlink(absPath);
|
|
1505
2755
|
return;
|
|
1506
2756
|
}
|
|
1507
2757
|
} catch {
|
|
1508
2758
|
}
|
|
1509
|
-
await
|
|
2759
|
+
await fs5.remove(absPath);
|
|
1510
2760
|
}
|
|
1511
2761
|
/**
|
|
1512
2762
|
* Installed-addon freshness (plan 003): after a child bundle (one with a
|
|
@@ -1535,9 +2785,9 @@ var UninstallEngine = class {
|
|
|
1535
2785
|
for (const artifact of projection) {
|
|
1536
2786
|
if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
|
|
1537
2787
|
if (artifact.content === void 0) continue;
|
|
1538
|
-
const dest =
|
|
1539
|
-
if (!await
|
|
1540
|
-
await
|
|
2788
|
+
const dest = path7.join(workspaceRoot, artifact.relPath);
|
|
2789
|
+
if (!await fs5.pathExists(dest)) continue;
|
|
2790
|
+
await fs5.writeFile(dest, artifact.content, "utf8");
|
|
1541
2791
|
const hash = await this.calculateHash(dest);
|
|
1542
2792
|
if (lockfile.projections?.[artifact.relPath]) {
|
|
1543
2793
|
lockfile.projections[artifact.relPath].hash = hash;
|
|
@@ -1559,15 +2809,15 @@ var UninstallEngine = class {
|
|
|
1559
2809
|
return options.scope || "project";
|
|
1560
2810
|
}
|
|
1561
2811
|
async safeRemove(targetDir, relPath) {
|
|
1562
|
-
const fullPath =
|
|
1563
|
-
if (relPath.startsWith("skills" +
|
|
2812
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
2813
|
+
if (relPath.startsWith("skills" + path7.sep) || relPath.startsWith("skills/")) {
|
|
1564
2814
|
const parts = relPath.split(/[/\\]/);
|
|
1565
2815
|
if (parts.length >= 2) {
|
|
1566
|
-
const skillDir =
|
|
2816
|
+
const skillDir = path7.join(targetDir, parts[0], parts[1]);
|
|
1567
2817
|
try {
|
|
1568
|
-
const lstat = await
|
|
2818
|
+
const lstat = await fs5.lstat(skillDir);
|
|
1569
2819
|
if (lstat.isSymbolicLink()) {
|
|
1570
|
-
await
|
|
2820
|
+
await fs5.unlink(skillDir);
|
|
1571
2821
|
return;
|
|
1572
2822
|
}
|
|
1573
2823
|
} catch {
|
|
@@ -1575,14 +2825,14 @@ var UninstallEngine = class {
|
|
|
1575
2825
|
}
|
|
1576
2826
|
}
|
|
1577
2827
|
try {
|
|
1578
|
-
const lstat = await
|
|
2828
|
+
const lstat = await fs5.lstat(fullPath);
|
|
1579
2829
|
if (lstat.isSymbolicLink()) {
|
|
1580
|
-
await
|
|
2830
|
+
await fs5.unlink(fullPath);
|
|
1581
2831
|
return;
|
|
1582
2832
|
}
|
|
1583
2833
|
} catch {
|
|
1584
2834
|
}
|
|
1585
|
-
await
|
|
2835
|
+
await fs5.remove(fullPath);
|
|
1586
2836
|
}
|
|
1587
2837
|
async uninstall(identifier, options = {}) {
|
|
1588
2838
|
const scope = this.parseScope(options);
|
|
@@ -1590,30 +2840,92 @@ var UninstallEngine = class {
|
|
|
1590
2840
|
const targetDirs = hosts.map((h) => AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
|
|
1591
2841
|
const resolved = await this.registry.resolve(identifier).catch(() => null);
|
|
1592
2842
|
const totalRemoved = [];
|
|
2843
|
+
const totalKept = [];
|
|
2844
|
+
const totalStale = [];
|
|
2845
|
+
const retainedOwners = /* @__PURE__ */ new Set();
|
|
1593
2846
|
for (const targetDir of targetDirs) {
|
|
1594
2847
|
const subPaths = AgentHostAdapter.getSubPaths(targetDir);
|
|
1595
|
-
if (!await
|
|
2848
|
+
if (!await fs5.pathExists(subPaths.lockfile)) {
|
|
1596
2849
|
continue;
|
|
1597
2850
|
}
|
|
1598
|
-
const lockfile = await
|
|
2851
|
+
const lockfile = await fs5.readJson(subPaths.lockfile);
|
|
1599
2852
|
const removedFiles = [];
|
|
2853
|
+
const keptFiles = [];
|
|
2854
|
+
const staleRecords = [];
|
|
2855
|
+
const projectionOwners = (projRelPath, proj) => {
|
|
2856
|
+
const own = proj.owners ?? [];
|
|
2857
|
+
if (projRelPath.startsWith(".agents/plugins/")) return own;
|
|
2858
|
+
const canonical = proj.canonical ?? "";
|
|
2859
|
+
const rec = lockfile.files[canonical] ?? lockfile.files[canonical.replace(/^\.agents\//, "")];
|
|
2860
|
+
return Array.from(/* @__PURE__ */ new Set([...own, ...assetOwners(rec)]));
|
|
2861
|
+
};
|
|
2862
|
+
const survivingCoverage = async (source, removedId) => {
|
|
2863
|
+
const survivors = lockfile.installed.bundles.filter((b) => b !== removedId);
|
|
2864
|
+
const covering = [];
|
|
2865
|
+
for (const id of survivors) {
|
|
2866
|
+
try {
|
|
2867
|
+
const def = await this.registry.getBundle(id);
|
|
2868
|
+
const have = /* @__PURE__ */ new Set([]);
|
|
2869
|
+
if (def) {
|
|
2870
|
+
if (def.orchestrator) have.add(`agents/${def.orchestrator}`);
|
|
2871
|
+
(def.agents || []).forEach((a) => have.add(`agents/${a}`));
|
|
2872
|
+
(def.skills || []).forEach((s) => have.add(`skills/${s}/SKILL.md`));
|
|
2873
|
+
(def.workflows || []).forEach((w) => have.add(`workflows/${w}`));
|
|
2874
|
+
(def.rules || []).forEach((rl) => have.add(`rules/${rl}`));
|
|
2875
|
+
} else {
|
|
2876
|
+
const r = await this.registry.resolve(id);
|
|
2877
|
+
r.agents.forEach((f) => have.add(`agents/${f}`));
|
|
2878
|
+
r.skills.forEach((s) => have.add(`skills/${s}/SKILL.md`));
|
|
2879
|
+
(r.workflows || []).forEach((w) => have.add(`workflows/${w}`));
|
|
2880
|
+
(r.rules || []).forEach((rl) => have.add(`rules/${rl}`));
|
|
2881
|
+
}
|
|
2882
|
+
if (source.asset) {
|
|
2883
|
+
if (have.has(source.asset)) covering.push(id);
|
|
2884
|
+
} else if (source.declaringBundle) {
|
|
2885
|
+
const def2 = await this.registry.getBundle(source.declaringBundle);
|
|
2886
|
+
if (!def2) continue;
|
|
2887
|
+
const want = /* @__PURE__ */ new Set([]);
|
|
2888
|
+
if (def2.orchestrator) want.add(`agents/${def2.orchestrator}`);
|
|
2889
|
+
(def2.agents || []).forEach((a) => want.add(`agents/${a}`));
|
|
2890
|
+
(def2.skills || []).forEach((s) => want.add(`skills/${s}/SKILL.md`));
|
|
2891
|
+
(def2.workflows || []).forEach((w) => want.add(`workflows/${w}`));
|
|
2892
|
+
(def2.rules || []).forEach((rl) => want.add(`rules/${rl}`));
|
|
2893
|
+
if (want.size > 0 && [...want].every((k) => have.has(k))) covering.push(id);
|
|
2894
|
+
}
|
|
2895
|
+
} catch {
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
return covering;
|
|
2899
|
+
};
|
|
1600
2900
|
if (resolved && resolved.targetBundle) {
|
|
1601
2901
|
const bundleName = resolved.targetBundle;
|
|
1602
|
-
|
|
2902
|
+
const ownsSomething = Object.values(lockfile.files).some((m) => assetOwners(m).includes(bundleName)) || Object.values(lockfile.projections || {}).some((p) => (p.owners ?? []).includes(bundleName));
|
|
2903
|
+
if (lockfile.installed.bundles.includes(bundleName) || ownsSomething) {
|
|
1603
2904
|
if (!options.dryRun) {
|
|
1604
|
-
const workspaceRoot =
|
|
1605
|
-
const ownedFiles = Object.entries(lockfile.files).filter(([, m]) => (m
|
|
1606
|
-
const ownedProjections = lockfile.projections ? Object.
|
|
2905
|
+
const workspaceRoot = path7.resolve(path7.dirname(targetDir));
|
|
2906
|
+
const ownedFiles = Object.entries(lockfile.files).filter(([, m]) => assetOwners(m).includes(bundleName));
|
|
2907
|
+
const ownedProjections = lockfile.projections ? Object.entries(lockfile.projections).filter(([p, v]) => projectionOwners(p, v).includes(bundleName)).length : 0;
|
|
1607
2908
|
if (ownedFiles.length === 0 && ownedProjections === 0) {
|
|
1608
2909
|
throw new Error(`No installed assets found matching "${bundleName}".`);
|
|
1609
2910
|
}
|
|
1610
2911
|
if (lockfile.projections) {
|
|
1611
2912
|
for (const [projRelPath, proj] of Object.entries(lockfile.projections)) {
|
|
1612
|
-
|
|
1613
|
-
|
|
2913
|
+
const owners = projectionOwners(projRelPath, proj);
|
|
2914
|
+
if (owners.includes(bundleName)) {
|
|
2915
|
+
proj.owners = owners.filter((o) => o !== bundleName);
|
|
1614
2916
|
if (proj.owners.length === 0) {
|
|
1615
|
-
const
|
|
1616
|
-
|
|
2917
|
+
const declaringMatch = /^\.agents\/plugins\/([^/]+)\//.exec(projRelPath);
|
|
2918
|
+
const derivedCoordination = Boolean(declaringMatch) || proj.kind === "rule" || proj.kind === "team-manifest" || proj.kind === "plugin-manifest";
|
|
2919
|
+
const covering = derivedCoordination ? await survivingCoverage({ declaringBundle: declaringMatch ? declaringMatch[1] : bundleName }, bundleName) : [];
|
|
2920
|
+
if (covering.length > 0) {
|
|
2921
|
+
proj.owners = covering;
|
|
2922
|
+
keptFiles.push(projRelPath);
|
|
2923
|
+
covering.forEach((o) => retainedOwners.add(o));
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
const absProjection = path7.join(workspaceRoot, projRelPath);
|
|
2927
|
+
const existedOnDisk = await fs5.pathExists(absProjection);
|
|
2928
|
+
if (existedOnDisk) {
|
|
1617
2929
|
if (proj.managedMarker) {
|
|
1618
2930
|
const managed = await this.isManagedProjection(absProjection, projRelPath === "AGENTS.md");
|
|
1619
2931
|
if (!managed && !options.force) {
|
|
@@ -1629,20 +2941,36 @@ var UninstallEngine = class {
|
|
|
1629
2941
|
await this.removeEmptyProjectionDirs(workspaceRoot, projRelPath);
|
|
1630
2942
|
}
|
|
1631
2943
|
delete lockfile.projections[projRelPath];
|
|
2944
|
+
if (existedOnDisk) {
|
|
2945
|
+
removedFiles.push(projRelPath);
|
|
2946
|
+
} else {
|
|
2947
|
+
staleRecords.push(projRelPath);
|
|
2948
|
+
}
|
|
2949
|
+
const canonicalKey = (proj.canonical ?? "").replace(/^\.agents\//, "");
|
|
2950
|
+
for (const key of /* @__PURE__ */ new Set([proj.canonical ?? "", canonicalKey])) {
|
|
2951
|
+
const rec = key ? lockfile.files[key] : void 0;
|
|
2952
|
+
if (rec?.projectedTo) {
|
|
2953
|
+
rec.projectedTo = rec.projectedTo.filter((p) => p !== projRelPath);
|
|
2954
|
+
if (rec.projectedTo.length === 0) delete rec.projectedTo;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
} else {
|
|
2958
|
+
keptFiles.push(projRelPath);
|
|
2959
|
+
proj.owners.forEach((o) => retainedOwners.add(o));
|
|
1632
2960
|
}
|
|
1633
2961
|
}
|
|
1634
2962
|
}
|
|
1635
2963
|
}
|
|
1636
2964
|
for (const [relPath, assetMeta] of Object.entries(lockfile.files)) {
|
|
1637
|
-
const
|
|
1638
|
-
if (!
|
|
1639
|
-
const newOwners =
|
|
2965
|
+
const recordOwners = assetOwners(assetMeta);
|
|
2966
|
+
if (!recordOwners.includes(bundleName)) continue;
|
|
2967
|
+
const newOwners = recordOwners.filter((o) => o !== bundleName);
|
|
1640
2968
|
if (newOwners.length === 0) {
|
|
1641
2969
|
if (assetMeta.projectedTo && assetMeta.projectedTo.length > 0) {
|
|
1642
|
-
await this.removeProjections(workspaceRoot, assetMeta.projectedTo, options.force);
|
|
2970
|
+
await this.removeProjections(workspaceRoot, assetMeta.projectedTo, options.force, lockfile.projections);
|
|
1643
2971
|
}
|
|
1644
|
-
const fullPath =
|
|
1645
|
-
if (await
|
|
2972
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
2973
|
+
if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
|
|
1646
2974
|
if (!options.force && assetMeta.method !== "symlink") {
|
|
1647
2975
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
1648
2976
|
if (currentHash && currentHash !== assetMeta.hash) {
|
|
@@ -1655,6 +2983,16 @@ var UninstallEngine = class {
|
|
|
1655
2983
|
delete lockfile.files[relPath];
|
|
1656
2984
|
} else {
|
|
1657
2985
|
lockfile.files[relPath] = { ...assetMeta, owners: newOwners };
|
|
2986
|
+
keptFiles.push(relPath);
|
|
2987
|
+
newOwners.forEach((o) => retainedOwners.add(o));
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
for (const dir of [subPaths.agentsDir, subPaths.skillsDir, subPaths.workflowsDir, subPaths.rulesDir]) {
|
|
2991
|
+
if (await fs5.pathExists(dir)) {
|
|
2992
|
+
const entries = await fs5.readdir(dir).catch(() => []);
|
|
2993
|
+
if (entries.length === 0) {
|
|
2994
|
+
await fs5.remove(dir);
|
|
2995
|
+
}
|
|
1658
2996
|
}
|
|
1659
2997
|
}
|
|
1660
2998
|
lockfile.installed.bundles = lockfile.installed.bundles.filter((b) => b !== bundleName);
|
|
@@ -1665,7 +3003,12 @@ var UninstallEngine = class {
|
|
|
1665
3003
|
const survival = /* @__PURE__ */ new Set();
|
|
1666
3004
|
for (const b of surviving) {
|
|
1667
3005
|
const bdef = await this.registry.getBundle(b);
|
|
1668
|
-
if (!bdef)
|
|
3006
|
+
if (!bdef) {
|
|
3007
|
+
for (const [relPath, meta] of Object.entries(lockfile.files)) {
|
|
3008
|
+
if (assetOwners(meta).includes(b)) survival.add(relPath);
|
|
3009
|
+
}
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
1669
3012
|
if (bdef.orchestrator) survival.add(`agents/${bdef.orchestrator}`);
|
|
1670
3013
|
(bdef.agents || []).forEach((a) => survival.add(`agents/${a}`));
|
|
1671
3014
|
(bdef.skills || []).forEach((s) => survival.add(`skills/${s}/SKILL.md`));
|
|
@@ -1673,19 +3016,34 @@ var UninstallEngine = class {
|
|
|
1673
3016
|
}
|
|
1674
3017
|
lockfile.installed.agents = lockfile.installed.agents.filter((a) => survival.has(`agents/${a}`));
|
|
1675
3018
|
lockfile.installed.skills = lockfile.installed.skills.filter((s) => survival.has(`skills/${s}/SKILL.md`));
|
|
1676
|
-
|
|
3019
|
+
if (lockfile.installed.workflows) {
|
|
3020
|
+
lockfile.installed.workflows = lockfile.installed.workflows.filter((w) => survival.has(`workflows/${w}`));
|
|
3021
|
+
}
|
|
1677
3022
|
await this.refreshParentCoordination(bundleName, workspaceRoot, lockfile, scope);
|
|
1678
|
-
await
|
|
3023
|
+
await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
1679
3024
|
} else {
|
|
1680
|
-
|
|
3025
|
+
for (const [relPath, m] of Object.entries(lockfile.files)) {
|
|
3026
|
+
const owners = assetOwners(m);
|
|
3027
|
+
if (!owners.includes(bundleName)) continue;
|
|
3028
|
+
const remaining = owners.filter((o) => o !== bundleName);
|
|
3029
|
+
(remaining.length === 0 ? removedFiles : keptFiles).push(relPath);
|
|
3030
|
+
remaining.forEach((o) => retainedOwners.add(o));
|
|
3031
|
+
}
|
|
3032
|
+
for (const [projRelPath, proj] of Object.entries(lockfile.projections || {})) {
|
|
3033
|
+
const owners = projectionOwners(projRelPath, proj);
|
|
3034
|
+
if (!owners.includes(bundleName)) continue;
|
|
3035
|
+
const remaining = owners.filter((o) => o !== bundleName);
|
|
3036
|
+
(remaining.length === 0 ? removedFiles : keptFiles).push(projRelPath);
|
|
3037
|
+
remaining.forEach((o) => retainedOwners.add(o));
|
|
3038
|
+
}
|
|
1681
3039
|
}
|
|
1682
3040
|
}
|
|
1683
3041
|
} else {
|
|
1684
3042
|
const itemTargetRelPaths = Object.keys(lockfile.files).filter((p) => p.includes(identifier));
|
|
1685
3043
|
if (!options.dryRun) {
|
|
1686
3044
|
for (const relPath of itemTargetRelPaths) {
|
|
1687
|
-
const fullPath =
|
|
1688
|
-
if (await
|
|
3045
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
3046
|
+
if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
|
|
1689
3047
|
if (!options.force && lockfile.files[relPath]?.method !== "symlink") {
|
|
1690
3048
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
1691
3049
|
const assetMeta = lockfile.files[relPath];
|
|
@@ -1698,24 +3056,33 @@ var UninstallEngine = class {
|
|
|
1698
3056
|
}
|
|
1699
3057
|
delete lockfile.files[relPath];
|
|
1700
3058
|
}
|
|
1701
|
-
await
|
|
3059
|
+
await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
1702
3060
|
} else {
|
|
1703
3061
|
removedFiles.push(...itemTargetRelPaths);
|
|
1704
3062
|
}
|
|
1705
3063
|
}
|
|
1706
3064
|
totalRemoved.push(...removedFiles);
|
|
3065
|
+
totalKept.push(...keptFiles);
|
|
3066
|
+
totalStale.push(...staleRecords);
|
|
1707
3067
|
}
|
|
1708
|
-
if (totalRemoved.length === 0 && !options.dryRun) {
|
|
3068
|
+
if (totalRemoved.length === 0 && totalKept.length === 0 && totalStale.length === 0 && !options.dryRun) {
|
|
1709
3069
|
throw new Error(`No installed assets found matching "${identifier}".`);
|
|
1710
3070
|
}
|
|
1711
|
-
return {
|
|
3071
|
+
return {
|
|
3072
|
+
removed: totalRemoved,
|
|
3073
|
+
kept: totalKept,
|
|
3074
|
+
staleRecords: totalStale,
|
|
3075
|
+
retainedOwners: [...retainedOwners].sort(),
|
|
3076
|
+
targetDirs,
|
|
3077
|
+
dryRun: options.dryRun || false
|
|
3078
|
+
};
|
|
1712
3079
|
}
|
|
1713
3080
|
};
|
|
1714
3081
|
|
|
1715
3082
|
// src/core/inventory.ts
|
|
1716
|
-
import
|
|
3083
|
+
import path8 from "path";
|
|
1717
3084
|
import os2 from "os";
|
|
1718
|
-
import
|
|
3085
|
+
import fs6 from "fs-extra";
|
|
1719
3086
|
var InventoryScanner = class _InventoryScanner {
|
|
1720
3087
|
registry;
|
|
1721
3088
|
constructor(registry2) {
|
|
@@ -1726,10 +3093,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
1726
3093
|
const cwd = process.cwd();
|
|
1727
3094
|
let prettyPath = targetDir;
|
|
1728
3095
|
if (targetDir.startsWith(cwd)) {
|
|
1729
|
-
const rel =
|
|
3096
|
+
const rel = path8.relative(cwd, targetDir);
|
|
1730
3097
|
prettyPath = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
1731
3098
|
} else if (targetDir.startsWith(home)) {
|
|
1732
|
-
const rel =
|
|
3099
|
+
const rel = path8.relative(home, targetDir);
|
|
1733
3100
|
prettyPath = `~/${rel.replace(/\\/g, "/")}`;
|
|
1734
3101
|
}
|
|
1735
3102
|
const fanoutSuffix = fanout && fanout.length > 0 ? ` + ${fanout.join(", ")}` : "";
|
|
@@ -1759,7 +3126,7 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
1759
3126
|
const candidateDirs = [];
|
|
1760
3127
|
if (options.targetDir) {
|
|
1761
3128
|
candidateDirs.push({
|
|
1762
|
-
dir:
|
|
3129
|
+
dir: path8.resolve(options.targetDir),
|
|
1763
3130
|
scope: options.scope || "project",
|
|
1764
3131
|
host: hosts[0] || "agents"
|
|
1765
3132
|
});
|
|
@@ -1781,10 +3148,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
1781
3148
|
const canonicalDir = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
|
|
1782
3149
|
let displayLoc = canonicalDir;
|
|
1783
3150
|
if (canonicalDir.startsWith(cwd)) {
|
|
1784
|
-
const rel =
|
|
3151
|
+
const rel = path8.relative(cwd, canonicalDir);
|
|
1785
3152
|
displayLoc = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
1786
3153
|
} else if (canonicalDir.startsWith(home)) {
|
|
1787
|
-
const rel =
|
|
3154
|
+
const rel = path8.relative(home, canonicalDir);
|
|
1788
3155
|
displayLoc = `~/${rel.replace(/\\/g, "/")}`;
|
|
1789
3156
|
}
|
|
1790
3157
|
scannedLocationsMap.set(`${scope}:${canonicalDir}`, {
|
|
@@ -1796,14 +3163,14 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
1796
3163
|
});
|
|
1797
3164
|
}
|
|
1798
3165
|
for (const item of candidateDirs) {
|
|
1799
|
-
const lockfilePath =
|
|
1800
|
-
if (!await
|
|
3166
|
+
const lockfilePath = path8.join(item.dir, "agents-united.json");
|
|
3167
|
+
if (!await fs6.pathExists(lockfilePath)) {
|
|
1801
3168
|
continue;
|
|
1802
3169
|
}
|
|
1803
3170
|
scannedTargetDirs.add(item.dir);
|
|
1804
3171
|
let lockfile;
|
|
1805
3172
|
try {
|
|
1806
|
-
lockfile = await
|
|
3173
|
+
lockfile = await fs6.readJson(lockfilePath);
|
|
1807
3174
|
} catch {
|
|
1808
3175
|
continue;
|
|
1809
3176
|
}
|
|
@@ -1952,10 +3319,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
1952
3319
|
};
|
|
1953
3320
|
|
|
1954
3321
|
// src/core/updater.ts
|
|
1955
|
-
import
|
|
3322
|
+
import path9 from "path";
|
|
1956
3323
|
import crypto3 from "crypto";
|
|
1957
|
-
import
|
|
1958
|
-
var UpdateEngine = class {
|
|
3324
|
+
import fs7 from "fs-extra";
|
|
3325
|
+
var UpdateEngine = class _UpdateEngine {
|
|
1959
3326
|
registry;
|
|
1960
3327
|
scanner;
|
|
1961
3328
|
installer;
|
|
@@ -1977,7 +3344,7 @@ var UpdateEngine = class {
|
|
|
1977
3344
|
return deduplicated;
|
|
1978
3345
|
}
|
|
1979
3346
|
async calculateHash(filePath) {
|
|
1980
|
-
const buffer = await
|
|
3347
|
+
const buffer = await fs7.readFile(filePath);
|
|
1981
3348
|
return "sha256:" + crypto3.createHash("sha256").update(buffer).digest("hex");
|
|
1982
3349
|
}
|
|
1983
3350
|
/**
|
|
@@ -1985,26 +3352,31 @@ var UpdateEngine = class {
|
|
|
1985
3352
|
* M5). Agent projections place the marker as the first body line after frontmatter;
|
|
1986
3353
|
* the AGENTS.md bridge has no frontmatter so it is checked over the whole content.
|
|
1987
3354
|
*/
|
|
1988
|
-
async isManagedProjection(absPath, isAgentsMd) {
|
|
1989
|
-
const content = await
|
|
3355
|
+
async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
3356
|
+
const content = await fs7.readFile(absPath, "utf8");
|
|
1990
3357
|
if (isAgentsMd) {
|
|
1991
3358
|
return content.includes("managed-by: agents-united");
|
|
1992
3359
|
}
|
|
1993
|
-
|
|
3360
|
+
if (HostProjector.hasManagedMarker(content)) return true;
|
|
3361
|
+
if (recordedHash) {
|
|
3362
|
+
const bytes = await fs7.readFile(absPath);
|
|
3363
|
+
return `sha256:${crypto3.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
|
|
3364
|
+
}
|
|
3365
|
+
return false;
|
|
1994
3366
|
}
|
|
1995
3367
|
/**
|
|
1996
3368
|
* Returns the first user-modified projection path belonging to a record, or '' when
|
|
1997
3369
|
* every recorded projection still carries the managed marker.
|
|
1998
3370
|
*/
|
|
1999
3371
|
async staleProjection(record, lockfile) {
|
|
2000
|
-
let workspaceRoot =
|
|
3372
|
+
let workspaceRoot = path9.dirname(record.targetDir);
|
|
2001
3373
|
for (const assetMeta of Object.values(lockfile.files)) {
|
|
2002
3374
|
if (assetMeta.bundle !== record.name) continue;
|
|
2003
3375
|
if (!assetMeta.projectedTo || assetMeta.projectedTo.length === 0) continue;
|
|
2004
3376
|
for (const projPath of assetMeta.projectedTo) {
|
|
2005
|
-
const absProjection =
|
|
2006
|
-
if (await
|
|
2007
|
-
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md");
|
|
3377
|
+
const absProjection = path9.join(workspaceRoot, projPath);
|
|
3378
|
+
if (await fs7.pathExists(absProjection)) {
|
|
3379
|
+
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md", lockfile.projections?.[projPath]?.hash);
|
|
2008
3380
|
if (!managed) {
|
|
2009
3381
|
return projPath;
|
|
2010
3382
|
}
|
|
@@ -2013,6 +3385,79 @@ var UpdateEngine = class {
|
|
|
2013
3385
|
}
|
|
2014
3386
|
return "";
|
|
2015
3387
|
}
|
|
3388
|
+
/** Plan 016 Step 4 — the canonical asset whose dialect name changed (ADR 0016 pattern). */
|
|
3389
|
+
static LEGACY_GENERATIVE_UI_CANONICAL = "skills/generative_ui/SKILL.md";
|
|
3390
|
+
static LEGACY_GENERATIVE_UI_PATH = /(^|\/)generative_ui\/SKILL\.md$/;
|
|
3391
|
+
/** Removes empty projection dirs left behind by the migration (never a non-empty one). */
|
|
3392
|
+
async removeEmptyProjectionDirs(workspaceRoot, projPath) {
|
|
3393
|
+
let dir = path9.dirname(path9.join(workspaceRoot, projPath));
|
|
3394
|
+
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
3395
|
+
try {
|
|
3396
|
+
const entries = await fs7.readdir(dir);
|
|
3397
|
+
if (entries.length > 0) break;
|
|
3398
|
+
await fs7.rmdir(dir);
|
|
3399
|
+
} catch {
|
|
3400
|
+
break;
|
|
3401
|
+
}
|
|
3402
|
+
dir = path9.dirname(dir);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
/** Drops one projected path from every canonical file record (host-agnostic). */
|
|
3406
|
+
dropProjectedTo(lockfile, projectedRelPath) {
|
|
3407
|
+
const normProj = projectedRelPath.replace(/\\/g, "/");
|
|
3408
|
+
for (const asset of Object.values(lockfile.files)) {
|
|
3409
|
+
if (!asset.projectedTo || asset.projectedTo.length === 0) continue;
|
|
3410
|
+
const kept = asset.projectedTo.filter((p) => p !== normProj);
|
|
3411
|
+
if (kept.length === asset.projectedTo.length) continue;
|
|
3412
|
+
if (kept.length === 0) {
|
|
3413
|
+
delete asset.projectedTo;
|
|
3414
|
+
} else {
|
|
3415
|
+
asset.projectedTo = kept;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
/**
|
|
3420
|
+
* Plan 016 Step 4 — silent, bounded, idempotent migration of the `generative_ui`
|
|
3421
|
+
* projection rename (ADR 0016 pattern).
|
|
3422
|
+
*
|
|
3423
|
+
* `registry/skills/generative_ui/` is deliberately NOT renamed: the underscore is
|
|
3424
|
+
* invalid in every host dialect, so the projectors normalize the name at projection
|
|
3425
|
+
* time (`generative_ui` -> `generative-ui`; ADR 0018 decision 2 — normalization is
|
|
3426
|
+
* projection-level). Projections recorded before that change therefore still point at
|
|
3427
|
+
* `.claude/skills/generative_ui/SKILL.md` and/or the Cline plugin copy
|
|
3428
|
+
* `.agents/plugins/<bundle>/skills/generative_ui/SKILL.md`, which the current renderers
|
|
3429
|
+
* no longer produce.
|
|
3430
|
+
*
|
|
3431
|
+
* Runs before each record's re-install so the freshly rendered normalized path is
|
|
3432
|
+
* registered by the normal install path. Guarantees: (a) a file that lacks the managed
|
|
3433
|
+
* marker is never deleted (its lockfile entries are left untouched too, so the
|
|
3434
|
+
* migration never lies about a file it did not remove); (b) a second run is a no-op
|
|
3435
|
+
* because the entries are gone; (c) it is host-agnostic, so it covers `.claude/**` and
|
|
3436
|
+
* the Cline plugin lane alike, and it never touches the canonical registry store.
|
|
3437
|
+
*/
|
|
3438
|
+
async migrateLegacyGenerativeUiProjections(workspaceRoot, lockfile) {
|
|
3439
|
+
const projections = lockfile.projections;
|
|
3440
|
+
if (!projections) return [];
|
|
3441
|
+
const normalized = ClaudeProjector.normalizeSkillName("generative_ui");
|
|
3442
|
+
const removed = [];
|
|
3443
|
+
for (const [projRelPath, proj] of Object.entries(projections)) {
|
|
3444
|
+
const posixPath = projRelPath.replace(/\\/g, "/");
|
|
3445
|
+
const isLegacy = proj.canonical === _UpdateEngine.LEGACY_GENERATIVE_UI_CANONICAL || _UpdateEngine.LEGACY_GENERATIVE_UI_PATH.test(posixPath);
|
|
3446
|
+
if (!isLegacy) continue;
|
|
3447
|
+
if (posixPath.includes(`/${normalized}/`)) continue;
|
|
3448
|
+
const absProjection = path9.join(workspaceRoot, projRelPath);
|
|
3449
|
+
if (await fs7.pathExists(absProjection)) {
|
|
3450
|
+
const managed = await this.isManagedProjection(absProjection, posixPath === "AGENTS.md");
|
|
3451
|
+
if (!managed) continue;
|
|
3452
|
+
await fs7.remove(absProjection);
|
|
3453
|
+
await this.removeEmptyProjectionDirs(workspaceRoot, projRelPath);
|
|
3454
|
+
}
|
|
3455
|
+
delete projections[projRelPath];
|
|
3456
|
+
this.dropProjectedTo(lockfile, projRelPath);
|
|
3457
|
+
removed.push(projRelPath);
|
|
3458
|
+
}
|
|
3459
|
+
return removed;
|
|
3460
|
+
}
|
|
2016
3461
|
async checkUpdates(options = {}) {
|
|
2017
3462
|
const inventory = await this.scanner.scan(options);
|
|
2018
3463
|
const items = [];
|
|
@@ -2064,8 +3509,8 @@ var UpdateEngine = class {
|
|
|
2064
3509
|
if (options.dryRun) {
|
|
2065
3510
|
const dryProjections = [];
|
|
2066
3511
|
for (const record of recordsToUpdate) {
|
|
2067
|
-
const lockfilePath =
|
|
2068
|
-
if (await
|
|
3512
|
+
const lockfilePath = path9.join(record.targetDir, "agents-united.json");
|
|
3513
|
+
if (await fs7.pathExists(lockfilePath)) {
|
|
2069
3514
|
const dryResult = await this.installer.install(record.name, {
|
|
2070
3515
|
scope: record.scope,
|
|
2071
3516
|
method: record.method,
|
|
@@ -2091,18 +3536,18 @@ var UpdateEngine = class {
|
|
|
2091
3536
|
const skipped = [];
|
|
2092
3537
|
const collectedProjections = [];
|
|
2093
3538
|
for (const record of recordsToUpdate) {
|
|
2094
|
-
const lockfilePath =
|
|
2095
|
-
if (!await
|
|
3539
|
+
const lockfilePath = path9.join(record.targetDir, "agents-united.json");
|
|
3540
|
+
if (!await fs7.pathExists(lockfilePath)) {
|
|
2096
3541
|
continue;
|
|
2097
3542
|
}
|
|
2098
|
-
const lockfile = await
|
|
3543
|
+
const lockfile = await fs7.readJson(lockfilePath);
|
|
2099
3544
|
if (record.method === "copy" && !options.force && lockfile.files) {
|
|
2100
3545
|
let hasConflict = false;
|
|
2101
3546
|
let conflictRelPath = "";
|
|
2102
3547
|
for (const [relPath, assetMeta] of Object.entries(lockfile.files)) {
|
|
2103
3548
|
if (assetMeta.bundle === record.name || relPath.includes(record.name)) {
|
|
2104
|
-
const fullPath =
|
|
2105
|
-
if (await
|
|
3549
|
+
const fullPath = path9.join(record.targetDir, relPath);
|
|
3550
|
+
if (await fs7.pathExists(fullPath)) {
|
|
2106
3551
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
2107
3552
|
if (currentHash && currentHash !== assetMeta.hash) {
|
|
2108
3553
|
hasConflict = true;
|
|
@@ -2130,6 +3575,13 @@ var UpdateEngine = class {
|
|
|
2130
3575
|
continue;
|
|
2131
3576
|
}
|
|
2132
3577
|
}
|
|
3578
|
+
const migratedGenerativeUi = await this.migrateLegacyGenerativeUiProjections(
|
|
3579
|
+
path9.dirname(record.targetDir),
|
|
3580
|
+
lockfile
|
|
3581
|
+
);
|
|
3582
|
+
if (migratedGenerativeUi.length > 0) {
|
|
3583
|
+
await fs7.writeJson(lockfilePath, lockfile, { spaces: 2 });
|
|
3584
|
+
}
|
|
2133
3585
|
const installResult = await this.installer.install(record.name, {
|
|
2134
3586
|
scope: record.scope,
|
|
2135
3587
|
method: record.method,
|
|
@@ -2142,11 +3594,11 @@ var UpdateEngine = class {
|
|
|
2142
3594
|
if (installResult.projections) {
|
|
2143
3595
|
collectedProjections.push(...installResult.projections);
|
|
2144
3596
|
}
|
|
2145
|
-
if (await
|
|
2146
|
-
const updatedLockfile = await
|
|
3597
|
+
if (await fs7.pathExists(lockfilePath)) {
|
|
3598
|
+
const updatedLockfile = await fs7.readJson(lockfilePath);
|
|
2147
3599
|
updatedLockfile.bundleVersions = updatedLockfile.bundleVersions || {};
|
|
2148
3600
|
updatedLockfile.bundleVersions[record.name] = record.upstreamVersion;
|
|
2149
|
-
await
|
|
3601
|
+
await fs7.writeJson(lockfilePath, updatedLockfile, { spaces: 2 });
|
|
2150
3602
|
if (updatedLockfile.fanout) {
|
|
2151
3603
|
record.fanout = updatedLockfile.fanout;
|
|
2152
3604
|
record.projections = updatedLockfile.fanout;
|
|
@@ -2173,13 +3625,18 @@ var UpdateEngine = class {
|
|
|
2173
3625
|
};
|
|
2174
3626
|
|
|
2175
3627
|
// src/core/doctor.ts
|
|
2176
|
-
import
|
|
2177
|
-
import
|
|
2178
|
-
import
|
|
3628
|
+
import path12 from "path";
|
|
3629
|
+
import fs10 from "fs-extra";
|
|
3630
|
+
import crypto4 from "crypto";
|
|
3631
|
+
import YAML3 from "yaml";
|
|
3632
|
+
|
|
3633
|
+
// src/core/claude-capabilities.ts
|
|
3634
|
+
import path11 from "path";
|
|
3635
|
+
import fs9 from "fs-extra";
|
|
2179
3636
|
|
|
2180
3637
|
// src/core/cline-capabilities.ts
|
|
2181
|
-
import
|
|
2182
|
-
import
|
|
3638
|
+
import path10 from "path";
|
|
3639
|
+
import fs8 from "fs-extra";
|
|
2183
3640
|
import { execFile } from "child_process";
|
|
2184
3641
|
var defaultProcessRunner = (executable, args, options) => {
|
|
2185
3642
|
return new Promise((resolve) => {
|
|
@@ -2236,7 +3693,7 @@ var ClineCapabilityProbe = class {
|
|
|
2236
3693
|
return this.customResolver();
|
|
2237
3694
|
}
|
|
2238
3695
|
const envBin = process.env.CLINE_BIN_PATH;
|
|
2239
|
-
if (envBin &&
|
|
3696
|
+
if (envBin && path10.isAbsolute(envBin) && fs8.existsSync(envBin)) {
|
|
2240
3697
|
return {
|
|
2241
3698
|
executable: envBin,
|
|
2242
3699
|
prefixArgs: [],
|
|
@@ -2250,25 +3707,25 @@ var ClineCapabilityProbe = class {
|
|
|
2250
3707
|
const exeNames = ["cline.cmd", "cline.bat", "cline.exe", "cline"];
|
|
2251
3708
|
for (const dir of dirs) {
|
|
2252
3709
|
for (const exeName of exeNames) {
|
|
2253
|
-
const fullPath =
|
|
2254
|
-
if (
|
|
2255
|
-
const adjacentWrapper1 =
|
|
2256
|
-
const adjacentWrapper2 =
|
|
2257
|
-
if (
|
|
3710
|
+
const fullPath = path10.join(dir, exeName);
|
|
3711
|
+
if (fs8.existsSync(fullPath)) {
|
|
3712
|
+
const adjacentWrapper1 = path10.join(dir, "node_modules", "cline", "bin", "cline");
|
|
3713
|
+
const adjacentWrapper2 = path10.join(dir, "node_modules", "@cline", "cline", "bin", "cline");
|
|
3714
|
+
if (fs8.existsSync(adjacentWrapper1)) {
|
|
2258
3715
|
return {
|
|
2259
3716
|
executable: process.execPath,
|
|
2260
3717
|
prefixArgs: [adjacentWrapper1],
|
|
2261
3718
|
source: "node-wrapper"
|
|
2262
3719
|
};
|
|
2263
3720
|
}
|
|
2264
|
-
if (
|
|
3721
|
+
if (fs8.existsSync(adjacentWrapper2)) {
|
|
2265
3722
|
return {
|
|
2266
3723
|
executable: process.execPath,
|
|
2267
3724
|
prefixArgs: [adjacentWrapper2],
|
|
2268
3725
|
source: "node-wrapper"
|
|
2269
3726
|
};
|
|
2270
3727
|
}
|
|
2271
|
-
const ext =
|
|
3728
|
+
const ext = path10.extname(fullPath).toLowerCase();
|
|
2272
3729
|
if (ext === ".cmd" || ext === ".bat") {
|
|
2273
3730
|
return {
|
|
2274
3731
|
executable: "cmd.exe",
|
|
@@ -2286,8 +3743,8 @@ var ClineCapabilityProbe = class {
|
|
|
2286
3743
|
}
|
|
2287
3744
|
} else {
|
|
2288
3745
|
for (const dir of dirs) {
|
|
2289
|
-
const fullPath =
|
|
2290
|
-
if (
|
|
3746
|
+
const fullPath = path10.join(dir, "cline");
|
|
3747
|
+
if (fs8.existsSync(fullPath)) {
|
|
2291
3748
|
return {
|
|
2292
3749
|
executable: fullPath,
|
|
2293
3750
|
prefixArgs: [],
|
|
@@ -2351,31 +3808,289 @@ var ClineCapabilityProbe = class {
|
|
|
2351
3808
|
if (probeRes.exitCode === 0) {
|
|
2352
3809
|
namedTeams = true;
|
|
2353
3810
|
} else {
|
|
2354
|
-
diagnostics.push(`Named teams probe returned non-zero (exit code ${probeRes.exitCode}). Falling back to adaptive session.`);
|
|
3811
|
+
diagnostics.push(`Named teams probe returned non-zero (exit code ${probeRes.exitCode}). Falling back to adaptive session.`);
|
|
3812
|
+
}
|
|
3813
|
+
} catch (err) {
|
|
3814
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3815
|
+
diagnostics.push(`Named teams probe failed: ${msg}`);
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
installed: true,
|
|
3819
|
+
version,
|
|
3820
|
+
command: cmd,
|
|
3821
|
+
namedTeams,
|
|
3822
|
+
rolePresetConsumer: "unknown",
|
|
3823
|
+
diagnostics
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
};
|
|
3827
|
+
|
|
3828
|
+
// src/core/claude-capabilities.ts
|
|
3829
|
+
var SUBAGENT_HANDBACK_MIN = { major: 2, minor: 1, patch: 271 };
|
|
3830
|
+
function supportsSubagentHandback(version) {
|
|
3831
|
+
if (!version) return false;
|
|
3832
|
+
const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
3833
|
+
if (!match) return false;
|
|
3834
|
+
const [major, minor, patch] = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3835
|
+
if (major !== SUBAGENT_HANDBACK_MIN.major) return major > SUBAGENT_HANDBACK_MIN.major;
|
|
3836
|
+
if (minor !== SUBAGENT_HANDBACK_MIN.minor) return minor > SUBAGENT_HANDBACK_MIN.minor;
|
|
3837
|
+
return patch >= SUBAGENT_HANDBACK_MIN.patch;
|
|
3838
|
+
}
|
|
3839
|
+
var ClaudeCapabilityProbe = class {
|
|
3840
|
+
runner;
|
|
3841
|
+
customResolver;
|
|
3842
|
+
constructor(runner, options) {
|
|
3843
|
+
this.runner = runner || defaultProcessRunner;
|
|
3844
|
+
this.customResolver = options?.resolveExecutable;
|
|
3845
|
+
}
|
|
3846
|
+
/**
|
|
3847
|
+
* Safe executable resolution across POSIX and Windows layouts.
|
|
3848
|
+
*/
|
|
3849
|
+
resolveExecutable() {
|
|
3850
|
+
if (this.customResolver) {
|
|
3851
|
+
return this.customResolver();
|
|
3852
|
+
}
|
|
3853
|
+
const envBin = process.env.CLAUDE_BIN_PATH;
|
|
3854
|
+
if (envBin && path11.isAbsolute(envBin) && fs9.existsSync(envBin)) {
|
|
3855
|
+
return {
|
|
3856
|
+
executable: envBin,
|
|
3857
|
+
prefixArgs: [],
|
|
3858
|
+
source: "env-binary"
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
const pathEnv = process.env.PATH || "";
|
|
3862
|
+
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
3863
|
+
const dirs = pathEnv.split(pathSeparator).filter(Boolean);
|
|
3864
|
+
if (process.platform === "win32") {
|
|
3865
|
+
const exeNames = ["claude.cmd", "claude.bat", "claude.exe", "claude"];
|
|
3866
|
+
for (const dir of dirs) {
|
|
3867
|
+
for (const exeName of exeNames) {
|
|
3868
|
+
const fullPath = path11.join(dir, exeName);
|
|
3869
|
+
if (fs9.existsSync(fullPath)) {
|
|
3870
|
+
const adjacentWrapper1 = path11.join(dir, "node_modules", "@anthropic-ai", "claude-code", "cli.js");
|
|
3871
|
+
const adjacentWrapper2 = path11.join(dir, "node_modules", "claude", "bin", "claude");
|
|
3872
|
+
if (fs9.existsSync(adjacentWrapper1)) {
|
|
3873
|
+
return {
|
|
3874
|
+
executable: process.execPath,
|
|
3875
|
+
prefixArgs: [adjacentWrapper1],
|
|
3876
|
+
source: "node-wrapper"
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
if (fs9.existsSync(adjacentWrapper2)) {
|
|
3880
|
+
return {
|
|
3881
|
+
executable: process.execPath,
|
|
3882
|
+
prefixArgs: [adjacentWrapper2],
|
|
3883
|
+
source: "node-wrapper"
|
|
3884
|
+
};
|
|
3885
|
+
}
|
|
3886
|
+
const ext = path11.extname(fullPath).toLowerCase();
|
|
3887
|
+
if (ext === ".cmd" || ext === ".bat") {
|
|
3888
|
+
return {
|
|
3889
|
+
executable: "cmd.exe",
|
|
3890
|
+
prefixArgs: ["/c", fullPath],
|
|
3891
|
+
source: "path-executable"
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3894
|
+
return {
|
|
3895
|
+
executable: fullPath,
|
|
3896
|
+
prefixArgs: [],
|
|
3897
|
+
source: "path-executable"
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
} else {
|
|
3903
|
+
for (const dir of dirs) {
|
|
3904
|
+
const fullPath = path11.join(dir, "claude");
|
|
3905
|
+
if (fs9.existsSync(fullPath)) {
|
|
3906
|
+
return {
|
|
3907
|
+
executable: fullPath,
|
|
3908
|
+
prefixArgs: [],
|
|
3909
|
+
source: "path-executable"
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
return null;
|
|
3915
|
+
}
|
|
3916
|
+
/**
|
|
3917
|
+
* Run the read-only capability probe against local Claude Code. Never throws: an absent
|
|
3918
|
+
* executable, a non-zero exit and a runner error are all reported as data.
|
|
3919
|
+
*/
|
|
3920
|
+
async probe() {
|
|
3921
|
+
const cmd = this.resolveExecutable();
|
|
3922
|
+
const diagnostics = [];
|
|
3923
|
+
if (!cmd) {
|
|
3924
|
+
diagnostics.push("Claude Code executable not found on PATH or via CLAUDE_BIN_PATH.");
|
|
3925
|
+
return {
|
|
3926
|
+
installed: false,
|
|
3927
|
+
pluginSupport: false,
|
|
3928
|
+
agentTeamsExperimental: false,
|
|
3929
|
+
subagentHandback: false,
|
|
3930
|
+
diagnostics
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
let version;
|
|
3934
|
+
let subagentHandback = false;
|
|
3935
|
+
try {
|
|
3936
|
+
const verRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "--version"], {
|
|
3937
|
+
timeoutMs: 5e3
|
|
3938
|
+
});
|
|
3939
|
+
if (verRes.exitCode === 0) {
|
|
3940
|
+
version = verRes.stdout.trim().split("\n")[0]?.trim();
|
|
3941
|
+
subagentHandback = supportsSubagentHandback(version);
|
|
3942
|
+
} else {
|
|
3943
|
+
diagnostics.push(`Claude Code version check failed (exit code ${verRes.exitCode}): ${verRes.stderr.trim()}`);
|
|
3944
|
+
return {
|
|
3945
|
+
installed: false,
|
|
3946
|
+
command: cmd,
|
|
3947
|
+
pluginSupport: false,
|
|
3948
|
+
agentTeamsExperimental: false,
|
|
3949
|
+
subagentHandback: false,
|
|
3950
|
+
diagnostics
|
|
3951
|
+
};
|
|
3952
|
+
}
|
|
3953
|
+
} catch (err) {
|
|
3954
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3955
|
+
diagnostics.push(`Error executing Claude Code version check: ${msg}`);
|
|
3956
|
+
return {
|
|
3957
|
+
installed: false,
|
|
3958
|
+
command: cmd,
|
|
3959
|
+
pluginSupport: false,
|
|
3960
|
+
agentTeamsExperimental: false,
|
|
3961
|
+
subagentHandback: false,
|
|
3962
|
+
diagnostics
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
let pluginSupport = false;
|
|
3966
|
+
let agentTeamsExperimental = false;
|
|
3967
|
+
try {
|
|
3968
|
+
const helpRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "--help"], {
|
|
3969
|
+
timeoutMs: 5e3
|
|
3970
|
+
});
|
|
3971
|
+
if (helpRes.exitCode === 0) {
|
|
3972
|
+
const help = helpRes.stdout;
|
|
3973
|
+
pluginSupport = help.includes("--plugin-dir");
|
|
3974
|
+
agentTeamsExperimental = /agent teams/i.test(help) || help.includes("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS");
|
|
3975
|
+
} else {
|
|
3976
|
+
diagnostics.push(
|
|
3977
|
+
`Claude Code help probe returned non-zero (exit code ${helpRes.exitCode}); plugin and agent-team support could not be verified.`
|
|
3978
|
+
);
|
|
2355
3979
|
}
|
|
2356
3980
|
} catch (err) {
|
|
2357
3981
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2358
|
-
diagnostics.push(`
|
|
3982
|
+
diagnostics.push(`Claude Code help probe failed: ${msg}`);
|
|
2359
3983
|
}
|
|
2360
3984
|
return {
|
|
2361
3985
|
installed: true,
|
|
2362
3986
|
version,
|
|
2363
3987
|
command: cmd,
|
|
2364
|
-
|
|
2365
|
-
|
|
3988
|
+
pluginSupport,
|
|
3989
|
+
agentTeamsExperimental,
|
|
3990
|
+
subagentHandback,
|
|
2366
3991
|
diagnostics
|
|
2367
3992
|
};
|
|
2368
3993
|
}
|
|
2369
3994
|
};
|
|
2370
3995
|
|
|
2371
3996
|
// src/core/doctor.ts
|
|
2372
|
-
var DoctorEngine = class {
|
|
2373
|
-
static async isManagedProjection(absPath, isAgentsMd) {
|
|
2374
|
-
const content = await
|
|
3997
|
+
var DoctorEngine = class _DoctorEngine {
|
|
3998
|
+
static async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
3999
|
+
const content = await fs10.readFile(absPath, "utf8");
|
|
2375
4000
|
if (isAgentsMd) {
|
|
2376
4001
|
return content.includes("managed-by: agents-united");
|
|
2377
4002
|
}
|
|
2378
|
-
|
|
4003
|
+
if (HostProjector.hasManagedMarker(content)) return true;
|
|
4004
|
+
if (recordedHash) {
|
|
4005
|
+
const bytes = await fs10.readFile(absPath);
|
|
4006
|
+
return `sha256:${crypto4.createHash("sha256").update(bytes).digest("hex")}` === recordedHash;
|
|
4007
|
+
}
|
|
4008
|
+
return false;
|
|
4009
|
+
}
|
|
4010
|
+
static isCompoundLaneHost(host) {
|
|
4011
|
+
return host === "cline" || host === "claude";
|
|
4012
|
+
}
|
|
4013
|
+
/**
|
|
4014
|
+
* Plan 015 §5.9 item 3 / ADR 0018 decision 2 — find the projection that now serves
|
|
4015
|
+
* `canonical` at a *different* path and actually exists on disk. A hit turns an absent
|
|
4016
|
+
* recorded path into self-healing staleness (`Stale projection … (superseded by …)`)
|
|
4017
|
+
* instead of a user-deletion report.
|
|
4018
|
+
*
|
|
4019
|
+
* Covers every rename the lockfile can still carry: the ADR 0016 workflow slug rename
|
|
4020
|
+
* (`.cline/workflows/implement-feature-or-fix.md` → `…/workflow-implement.md`) and the
|
|
4021
|
+
* ADR 0018 prefix strip (`.claude/agents/subagent-<role>.md` → `.claude/agents/<role>.md`).
|
|
4022
|
+
*/
|
|
4023
|
+
static async findSupersedingProjection(projPath, canonical, manifest, workspaceRoot) {
|
|
4024
|
+
if (!canonical) return null;
|
|
4025
|
+
for (const [projRelPath, proj] of Object.entries(manifest.projections ?? {})) {
|
|
4026
|
+
if (projRelPath === projPath) continue;
|
|
4027
|
+
if (proj.canonical !== canonical) continue;
|
|
4028
|
+
if (!await fs10.pathExists(path12.join(workspaceRoot, projRelPath))) continue;
|
|
4029
|
+
return { relPath: projRelPath, host: proj.host, owners: proj.owners ?? [] };
|
|
4030
|
+
}
|
|
4031
|
+
return null;
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* ADR 0017 — re-render the compound projections owned by the installed bundles so
|
|
4035
|
+
* doctor can compare what is on disk with what the current renderer produces.
|
|
4036
|
+
*
|
|
4037
|
+
* Returns projection path -> acceptable content variants **for `host` only**, or `null`
|
|
4038
|
+
* when the registry is unavailable (the freshness check is then skipped rather than
|
|
4039
|
+
* guessed).
|
|
4040
|
+
*
|
|
4041
|
+
* ADR 0018 decision 11 — the renderer is dispatched by the *recorded projection's* host.
|
|
4042
|
+
* Cline entries are diffed against `ClineProjector` output and Claude entries against
|
|
4043
|
+
* `ClaudeProjector` output; nothing else is comparable, and the Cline path is unchanged
|
|
4044
|
+
* because it renders exactly what it rendered before.
|
|
4045
|
+
*
|
|
4046
|
+
* Two variants are produced per path on purpose: the coordinator rule and team manifest
|
|
4047
|
+
* legitimately render either with or without the installed-bundle addon exclusions
|
|
4048
|
+
* (installer primary path vs parent-bundle refresh path). Matching *either* means the
|
|
4049
|
+
* projection is fresh, which keeps the check false-positive free while still catching
|
|
4050
|
+
* content that matches neither.
|
|
4051
|
+
*/
|
|
4052
|
+
static async renderProjectionVariants(owners, installedBundles, host) {
|
|
4053
|
+
if (owners.size === 0) return null;
|
|
4054
|
+
let resolver;
|
|
4055
|
+
try {
|
|
4056
|
+
resolver = new RegistryResolver();
|
|
4057
|
+
await resolver.loadBundles();
|
|
4058
|
+
} catch {
|
|
4059
|
+
return null;
|
|
4060
|
+
}
|
|
4061
|
+
const registryDir = resolver.getRegistryDir();
|
|
4062
|
+
const variants = /* @__PURE__ */ new Map();
|
|
4063
|
+
for (const owner of owners) {
|
|
4064
|
+
try {
|
|
4065
|
+
const bundleDef = await resolver.getBundle(owner);
|
|
4066
|
+
if (!bundleDef) continue;
|
|
4067
|
+
const resolved = await resolver.resolve(bundleDef.name);
|
|
4068
|
+
for (const excludeAddons of [void 0, installedBundles]) {
|
|
4069
|
+
const artifacts = host === "cline" ? await ClineProjector.planCompoundProjection(
|
|
4070
|
+
bundleDef,
|
|
4071
|
+
"project",
|
|
4072
|
+
resolved,
|
|
4073
|
+
registryDir,
|
|
4074
|
+
excludeAddons
|
|
4075
|
+
) : await ClaudeProjector.planCompoundProjection(
|
|
4076
|
+
bundleDef,
|
|
4077
|
+
"project",
|
|
4078
|
+
resolved,
|
|
4079
|
+
registryDir,
|
|
4080
|
+
excludeAddons
|
|
4081
|
+
);
|
|
4082
|
+
for (const artifact of artifacts) {
|
|
4083
|
+
if (artifact.content === void 0) continue;
|
|
4084
|
+
const list = variants.get(artifact.relPath) ?? [];
|
|
4085
|
+
if (!list.includes(artifact.content)) list.push(artifact.content);
|
|
4086
|
+
variants.set(artifact.relPath, list);
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
} catch {
|
|
4090
|
+
continue;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
return variants;
|
|
2379
4094
|
}
|
|
2380
4095
|
static async runDoctor(targetDir, host) {
|
|
2381
4096
|
const root = AgentHostAdapter.resolveHostDir("project", "agents", targetDir);
|
|
@@ -2387,16 +4102,19 @@ var DoctorEngine = class {
|
|
|
2387
4102
|
let workflowsCount = 0;
|
|
2388
4103
|
let manifest;
|
|
2389
4104
|
let clineCapability;
|
|
4105
|
+
let claudeCapability;
|
|
2390
4106
|
let isInitialized = false;
|
|
2391
|
-
const lockfileExists = await
|
|
2392
|
-
const agentsDirExists = await
|
|
4107
|
+
const lockfileExists = await fs10.pathExists(subPaths.lockfile);
|
|
4108
|
+
const agentsDirExists = await fs10.pathExists(subPaths.agentsDir);
|
|
2393
4109
|
if (lockfileExists) {
|
|
2394
4110
|
try {
|
|
2395
|
-
const parsed = await
|
|
4111
|
+
const parsed = await fs10.readJson(subPaths.lockfile);
|
|
2396
4112
|
manifest = parsed;
|
|
2397
4113
|
agentsCount = parsed.installed?.agents?.length || 0;
|
|
2398
4114
|
skillsCount = parsed.installed?.skills?.length || 0;
|
|
2399
|
-
|
|
4115
|
+
const legacyWorkflows = parsed.installed?.workflows?.length || 0;
|
|
4116
|
+
const workflowSkillCount = (parsed.installed?.skills || []).filter((s) => String(s).startsWith("workflow-")).length;
|
|
4117
|
+
workflowsCount = legacyWorkflows + workflowSkillCount;
|
|
2400
4118
|
isInitialized = true;
|
|
2401
4119
|
} catch (err) {
|
|
2402
4120
|
issues.push(`Corrupt lockfile at ${subPaths.lockfile}: ${err.message}`);
|
|
@@ -2404,17 +4122,17 @@ var DoctorEngine = class {
|
|
|
2404
4122
|
} else if (agentsDirExists) {
|
|
2405
4123
|
warnings.push(`No lockfile found at ${subPaths.lockfile}, but agent files were detected in ${subPaths.agentsDir}. Run 'agents update' to sync.`);
|
|
2406
4124
|
}
|
|
2407
|
-
if (await
|
|
2408
|
-
const agentFiles = await
|
|
4125
|
+
if (await fs10.pathExists(subPaths.agentsDir)) {
|
|
4126
|
+
const agentFiles = await fs10.readdir(subPaths.agentsDir);
|
|
2409
4127
|
for (const file of agentFiles) {
|
|
2410
4128
|
if (file.endsWith(".md")) {
|
|
2411
|
-
const content = await
|
|
4129
|
+
const content = await fs10.readFile(path12.join(subPaths.agentsDir, file), "utf8");
|
|
2412
4130
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
2413
4131
|
if (!frontmatterMatch) {
|
|
2414
4132
|
warnings.push(`Agent ${file} is missing YAML frontmatter.`);
|
|
2415
4133
|
} else {
|
|
2416
4134
|
try {
|
|
2417
|
-
const meta =
|
|
4135
|
+
const meta = YAML3.parse(frontmatterMatch[1]);
|
|
2418
4136
|
if (!meta.name) issues.push(`Agent ${file} missing 'name' in frontmatter.`);
|
|
2419
4137
|
if (!meta.description) warnings.push(`Agent ${file} missing 'description'.`);
|
|
2420
4138
|
if (!meta.model) warnings.push(`Agent ${file} missing 'model' definition.`);
|
|
@@ -2425,42 +4143,124 @@ var DoctorEngine = class {
|
|
|
2425
4143
|
}
|
|
2426
4144
|
}
|
|
2427
4145
|
}
|
|
4146
|
+
if (await fs10.pathExists(subPaths.workflowsDir)) {
|
|
4147
|
+
const remainingWorkflowFiles = (await fs10.readdir(subPaths.workflowsDir)).filter((f) => f.endsWith(".md"));
|
|
4148
|
+
if (remainingWorkflowFiles.length > 0) {
|
|
4149
|
+
warnings.push(`Detected ${remainingWorkflowFiles.length} legacy workflows in ${subPaths.workflowsDir}. Run 'agents update' to automatically migrate them to modern skills.`);
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
2428
4152
|
if (manifest) {
|
|
2429
|
-
const workspaceRoot =
|
|
4153
|
+
const workspaceRoot = path12.resolve(path12.dirname(root));
|
|
4154
|
+
const warnedProjectionPaths = /* @__PURE__ */ new Set();
|
|
4155
|
+
const pushProjectionWarning = (projPath, message) => {
|
|
4156
|
+
if (warnedProjectionPaths.has(projPath)) return;
|
|
4157
|
+
warnedProjectionPaths.add(projPath);
|
|
4158
|
+
warnings.push(message);
|
|
4159
|
+
};
|
|
2430
4160
|
for (const [relPath, assetMeta] of Object.entries(manifest.files)) {
|
|
2431
4161
|
const projectedTo = assetMeta.projectedTo;
|
|
2432
4162
|
if (!projectedTo || projectedTo.length === 0) continue;
|
|
2433
4163
|
for (const projPath of projectedTo) {
|
|
2434
|
-
const absProjection =
|
|
2435
|
-
if (!await
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
4164
|
+
const absProjection = path12.join(workspaceRoot, projPath);
|
|
4165
|
+
if (!await fs10.pathExists(absProjection)) {
|
|
4166
|
+
const superseding = await this.findSupersedingProjection(projPath, relPath, manifest, workspaceRoot);
|
|
4167
|
+
if (superseding) {
|
|
4168
|
+
const owner = superseding.owners[0];
|
|
4169
|
+
pushProjectionWarning(
|
|
4170
|
+
projPath,
|
|
4171
|
+
`Stale projection ${projPath} for canonical ${relPath} (superseded by ${superseding.relPath}).` + (owner ? ` Run: agents update ${owner} --fanout ${superseding.host} to reconcile.` : "")
|
|
4172
|
+
);
|
|
4173
|
+
} else {
|
|
4174
|
+
pushProjectionWarning(
|
|
4175
|
+
projPath,
|
|
4176
|
+
`Missing projection ${projPath} for canonical ${relPath}. Re-run: agents add ... --fanout`
|
|
4177
|
+
);
|
|
4178
|
+
}
|
|
2439
4179
|
continue;
|
|
2440
4180
|
}
|
|
2441
|
-
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md");
|
|
4181
|
+
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md", manifest.projections?.[projPath]?.hash);
|
|
2442
4182
|
if (!managed) {
|
|
2443
|
-
|
|
4183
|
+
pushProjectionWarning(projPath, `user-modified projection ${projPath}`);
|
|
2444
4184
|
}
|
|
2445
4185
|
}
|
|
2446
4186
|
}
|
|
2447
4187
|
if (manifest.projections) {
|
|
2448
4188
|
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
2449
|
-
const absPath =
|
|
2450
|
-
if (!await
|
|
2451
|
-
|
|
4189
|
+
const absPath = path12.join(workspaceRoot, projRelPath);
|
|
4190
|
+
if (!await fs10.pathExists(absPath)) {
|
|
4191
|
+
const superseding = await this.findSupersedingProjection(projRelPath, proj.canonical, manifest, workspaceRoot);
|
|
4192
|
+
if (superseding) {
|
|
4193
|
+
const owner = superseding.owners[0];
|
|
4194
|
+
pushProjectionWarning(
|
|
4195
|
+
projRelPath,
|
|
4196
|
+
`Stale projection ${projRelPath} for canonical ${proj.canonical} (superseded by ${superseding.relPath}).` + (owner ? ` Run: agents update ${owner} --fanout ${superseding.host} to reconcile.` : "")
|
|
4197
|
+
);
|
|
4198
|
+
} else {
|
|
4199
|
+
pushProjectionWarning(
|
|
4200
|
+
projRelPath,
|
|
4201
|
+
`Missing ${proj.host} projection ${projRelPath} (owners: ${proj.owners.join(", ")}).`
|
|
4202
|
+
);
|
|
4203
|
+
}
|
|
2452
4204
|
continue;
|
|
2453
4205
|
}
|
|
2454
4206
|
if (proj.managedMarker) {
|
|
2455
|
-
const managed = await this.isManagedProjection(absPath, projRelPath === "AGENTS.md");
|
|
4207
|
+
const managed = await this.isManagedProjection(absPath, projRelPath === "AGENTS.md", proj.hash);
|
|
2456
4208
|
if (!managed) {
|
|
2457
|
-
|
|
4209
|
+
pushProjectionWarning(projRelPath, `user-modified projection ${projRelPath}`);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
if (manifest.projections && manifest.installed) {
|
|
4215
|
+
const ownersByHost = /* @__PURE__ */ new Map();
|
|
4216
|
+
for (const proj of Object.values(manifest.projections)) {
|
|
4217
|
+
if (!_DoctorEngine.isCompoundLaneHost(proj.host)) continue;
|
|
4218
|
+
const owners = ownersByHost.get(proj.host) ?? /* @__PURE__ */ new Set();
|
|
4219
|
+
for (const owner of proj.owners) owners.add(owner);
|
|
4220
|
+
ownersByHost.set(proj.host, owners);
|
|
4221
|
+
}
|
|
4222
|
+
const variantsByHost = /* @__PURE__ */ new Map();
|
|
4223
|
+
for (const [projHost, owners] of ownersByHost) {
|
|
4224
|
+
const variants = await this.renderProjectionVariants(owners, manifest.installed.bundles ?? [], projHost);
|
|
4225
|
+
if (variants) variantsByHost.set(projHost, variants);
|
|
4226
|
+
}
|
|
4227
|
+
if (variantsByHost.size > 0) {
|
|
4228
|
+
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
4229
|
+
const projHost = proj.host;
|
|
4230
|
+
if (!_DoctorEngine.isCompoundLaneHost(projHost)) continue;
|
|
4231
|
+
const expectedVariants = variantsByHost.get(projHost);
|
|
4232
|
+
if (!expectedVariants) continue;
|
|
4233
|
+
const absPath = path12.join(workspaceRoot, projRelPath);
|
|
4234
|
+
if (!await fs10.pathExists(absPath)) continue;
|
|
4235
|
+
if (!proj.managedMarker) continue;
|
|
4236
|
+
const content = await fs10.readFile(absPath, "utf8");
|
|
4237
|
+
if (!HostProjector.hasManagedMarker(content)) continue;
|
|
4238
|
+
if (proj.hash) {
|
|
4239
|
+
const diskHash = `sha256:${crypto4.createHash("sha256").update(await fs10.readFile(absPath)).digest("hex")}`;
|
|
4240
|
+
if (diskHash !== proj.hash) {
|
|
4241
|
+
const owner = proj.owners[0];
|
|
4242
|
+
pushProjectionWarning(
|
|
4243
|
+
projRelPath,
|
|
4244
|
+
`Content drift ${projRelPath} \u2014 edited after installation (recorded ${proj.hash.slice(0, 16)}\u2026, on disk ${diskHash.slice(0, 16)}\u2026).` + (owner ? ` Run: agents update ${owner} --fanout ${proj.host} to restore it.` : "")
|
|
4245
|
+
);
|
|
4246
|
+
continue;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
const variants = expectedVariants.get(projRelPath);
|
|
4250
|
+
if (!variants || variants.length === 0) continue;
|
|
4251
|
+
const onDisk = content.replace(/\r\n/g, "\n");
|
|
4252
|
+
if (!variants.some((variant) => variant.replace(/\r\n/g, "\n") === onDisk)) {
|
|
4253
|
+
const owner = proj.owners[0];
|
|
4254
|
+
pushProjectionWarning(
|
|
4255
|
+
projRelPath,
|
|
4256
|
+
`Outdated projection ${projRelPath} (content differs from the current render).` + (owner ? ` Run: agents update ${owner} --fanout ${proj.host} to refresh it.` : "")
|
|
4257
|
+
);
|
|
2458
4258
|
}
|
|
2459
4259
|
}
|
|
2460
4260
|
}
|
|
2461
4261
|
}
|
|
2462
4262
|
const installedBundles = manifest.installed?.bundles ?? [];
|
|
2463
|
-
const fileOwnersOf = (rec) =>
|
|
4263
|
+
const fileOwnersOf = (rec) => assetOwners(rec);
|
|
2464
4264
|
if (manifest.projections) {
|
|
2465
4265
|
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
2466
4266
|
for (const owner of proj.owners) {
|
|
@@ -2490,6 +4290,12 @@ var DoctorEngine = class {
|
|
|
2490
4290
|
if (!clineCapability.installed) {
|
|
2491
4291
|
warnings.push("Cline executable was not detected on PATH or via CLINE_BIN_PATH.");
|
|
2492
4292
|
}
|
|
4293
|
+
} else if (host === "claude") {
|
|
4294
|
+
const probe = new ClaudeCapabilityProbe();
|
|
4295
|
+
claudeCapability = await probe.probe();
|
|
4296
|
+
if (!claudeCapability.installed) {
|
|
4297
|
+
warnings.push("Claude Code executable was not detected on PATH or via CLAUDE_BIN_PATH.");
|
|
4298
|
+
}
|
|
2493
4299
|
}
|
|
2494
4300
|
return {
|
|
2495
4301
|
valid: issues.length === 0,
|
|
@@ -2500,16 +4306,17 @@ var DoctorEngine = class {
|
|
|
2500
4306
|
agentsCount,
|
|
2501
4307
|
skillsCount,
|
|
2502
4308
|
workflowsCount,
|
|
2503
|
-
clineCapability
|
|
4309
|
+
clineCapability,
|
|
4310
|
+
claudeCapability
|
|
2504
4311
|
};
|
|
2505
4312
|
}
|
|
2506
4313
|
};
|
|
2507
4314
|
|
|
2508
4315
|
// src/core/cline-launcher.ts
|
|
2509
|
-
import
|
|
4316
|
+
import path13 from "path";
|
|
2510
4317
|
import os3 from "os";
|
|
2511
|
-
import
|
|
2512
|
-
import
|
|
4318
|
+
import crypto5 from "crypto";
|
|
4319
|
+
import fs11 from "fs-extra";
|
|
2513
4320
|
var ClineLauncher = class _ClineLauncher {
|
|
2514
4321
|
probe;
|
|
2515
4322
|
runner;
|
|
@@ -2522,7 +4329,7 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2522
4329
|
*/
|
|
2523
4330
|
static generateTeamName(bundleName, workspace) {
|
|
2524
4331
|
const sanitizedBundle = bundleName.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
2525
|
-
const hash =
|
|
4332
|
+
const hash = crypto5.createHash("sha256").update(path13.resolve(workspace)).digest("hex").slice(0, 8);
|
|
2526
4333
|
const prefix = `au-${sanitizedBundle}`;
|
|
2527
4334
|
const maxPrefixLen = 64 - 9;
|
|
2528
4335
|
const truncatedPrefix = prefix.slice(0, maxPrefixLen);
|
|
@@ -2549,16 +4356,16 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2549
4356
|
workspace = os3.homedir();
|
|
2550
4357
|
} else {
|
|
2551
4358
|
workspace = cwd;
|
|
2552
|
-
targetDir =
|
|
4359
|
+
targetDir = path13.join(cwd, ".agents");
|
|
2553
4360
|
scope = "project";
|
|
2554
4361
|
}
|
|
2555
|
-
const lockfilePath =
|
|
2556
|
-
if (!await
|
|
4362
|
+
const lockfilePath = path13.join(targetDir, "agents-united.json");
|
|
4363
|
+
if (!await fs11.pathExists(lockfilePath)) {
|
|
2557
4364
|
if (!isGlobal) {
|
|
2558
4365
|
const globalTarget = AgentHostAdapter.resolveHostDir("global", "agents");
|
|
2559
|
-
const globalLockfile =
|
|
2560
|
-
if (await
|
|
2561
|
-
const gLock = await
|
|
4366
|
+
const globalLockfile = path13.join(globalTarget, "agents-united.json");
|
|
4367
|
+
if (await fs11.pathExists(globalLockfile)) {
|
|
4368
|
+
const gLock = await fs11.readJson(globalLockfile);
|
|
2562
4369
|
if (gLock.installed?.bundles?.includes(bundleName)) {
|
|
2563
4370
|
return this.resolveInstallation(bundleName, { ...options, global: true });
|
|
2564
4371
|
}
|
|
@@ -2566,14 +4373,14 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2566
4373
|
}
|
|
2567
4374
|
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
2568
4375
|
}
|
|
2569
|
-
const lockfile = await
|
|
4376
|
+
const lockfile = await fs11.readJson(lockfilePath);
|
|
2570
4377
|
if (!lockfile.installed?.bundles?.includes(bundleName)) {
|
|
2571
4378
|
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
2572
4379
|
}
|
|
2573
4380
|
const hasClineFanout = (lockfile.fanout || []).includes("cline");
|
|
2574
4381
|
const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
|
|
2575
|
-
const manifestPath =
|
|
2576
|
-
if (!hasClineFanout || !await
|
|
4382
|
+
const manifestPath = path13.join(workspace, manifestRel);
|
|
4383
|
+
if (!hasClineFanout || !await fs11.pathExists(manifestPath)) {
|
|
2577
4384
|
throw new Error(
|
|
2578
4385
|
`Bundle '${bundleName}' is not projected to Cline. Run 'agents update ${bundleName} --fanout cline' first.`
|
|
2579
4386
|
);
|
|
@@ -2672,15 +4479,190 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2672
4479
|
}
|
|
2673
4480
|
};
|
|
2674
4481
|
|
|
4482
|
+
// src/core/claude-launcher.ts
|
|
4483
|
+
import path14 from "path";
|
|
4484
|
+
import os4 from "os";
|
|
4485
|
+
import fs12 from "fs-extra";
|
|
4486
|
+
var DEFAULT_COORDINATOR = "orchestrator-engineering.md";
|
|
4487
|
+
var ClaudeLauncher = class _ClaudeLauncher {
|
|
4488
|
+
probe;
|
|
4489
|
+
runner;
|
|
4490
|
+
constructor(probe, runner) {
|
|
4491
|
+
this.probe = probe || new ClaudeCapabilityProbe();
|
|
4492
|
+
this.runner = runner || defaultProcessRunner;
|
|
4493
|
+
}
|
|
4494
|
+
/**
|
|
4495
|
+
* The `--agent` value must equal the projected coordinator definition's basename without `.md`.
|
|
4496
|
+
*
|
|
4497
|
+
* A projected coordinator keeps its full `orchestrator-<x>` name in `.claude/agents/`: coordinators are
|
|
4498
|
+
* never `subagent-<x>` files, so the prefix stripping that applies to every specialist role is a no-op
|
|
4499
|
+
* here. We still route through the projector's own helper so the launcher and the renderer can never
|
|
4500
|
+
* diverge if a bundle ever declares a differently-prefixed orchestrator.
|
|
4501
|
+
*/
|
|
4502
|
+
static resolveCoordinatorName(orchestrator) {
|
|
4503
|
+
const file = (orchestrator && orchestrator.trim().length > 0 ? orchestrator.trim() : DEFAULT_COORDINATOR).replace(/\.md$/i, "");
|
|
4504
|
+
return ClaudeProjector.stripSubagentPrefix(path14.basename(file));
|
|
4505
|
+
}
|
|
4506
|
+
/**
|
|
4507
|
+
* Resolve an installed bundle for Claude Code activation. Mirrors `ClineLauncher.resolveInstallation`:
|
|
4508
|
+
* same global→project fallback, same `.agents/agents-united.json` lockfile path, same error text.
|
|
4509
|
+
*/
|
|
4510
|
+
async resolveInstallation(bundleName, options = {}) {
|
|
4511
|
+
const cwd = options.cwd || process.cwd();
|
|
4512
|
+
const isGlobal = !!options.global;
|
|
4513
|
+
let targetDir;
|
|
4514
|
+
let scope;
|
|
4515
|
+
let workspace;
|
|
4516
|
+
if (isGlobal) {
|
|
4517
|
+
targetDir = AgentHostAdapter.resolveHostDir("global", "agents");
|
|
4518
|
+
scope = "global";
|
|
4519
|
+
workspace = os4.homedir();
|
|
4520
|
+
} else {
|
|
4521
|
+
workspace = cwd;
|
|
4522
|
+
targetDir = path14.join(cwd, ".agents");
|
|
4523
|
+
scope = "project";
|
|
4524
|
+
}
|
|
4525
|
+
const lockfilePath = path14.join(targetDir, "agents-united.json");
|
|
4526
|
+
if (!await fs12.pathExists(lockfilePath)) {
|
|
4527
|
+
if (!isGlobal) {
|
|
4528
|
+
const globalTarget = AgentHostAdapter.resolveHostDir("global", "agents");
|
|
4529
|
+
const globalLockfile = path14.join(globalTarget, "agents-united.json");
|
|
4530
|
+
if (await fs12.pathExists(globalLockfile)) {
|
|
4531
|
+
const gLock = await fs12.readJson(globalLockfile);
|
|
4532
|
+
if (gLock.installed?.bundles?.includes(bundleName)) {
|
|
4533
|
+
return this.resolveInstallation(bundleName, { ...options, global: true });
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
4538
|
+
}
|
|
4539
|
+
const lockfile = await fs12.readJson(lockfilePath);
|
|
4540
|
+
if (!lockfile.installed?.bundles?.includes(bundleName)) {
|
|
4541
|
+
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
4542
|
+
}
|
|
4543
|
+
const hasClaudeFanout = (lockfile.fanout || []).includes("claude");
|
|
4544
|
+
const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
|
|
4545
|
+
const manifestPath = path14.join(workspace, manifestRel);
|
|
4546
|
+
if (!hasClaudeFanout && !await fs12.pathExists(manifestPath)) {
|
|
4547
|
+
throw new Error(
|
|
4548
|
+
`Bundle '${bundleName}' is not projected to Claude. Run 'agents update ${bundleName} --fanout claude' first.`
|
|
4549
|
+
);
|
|
4550
|
+
}
|
|
4551
|
+
return {
|
|
4552
|
+
scope,
|
|
4553
|
+
workspace,
|
|
4554
|
+
lockfile,
|
|
4555
|
+
manifestPath
|
|
4556
|
+
};
|
|
4557
|
+
}
|
|
4558
|
+
/**
|
|
4559
|
+
* Plan the argv, bootstrap prompt and ephemeral environment for a Claude Code session. Pure: no process
|
|
4560
|
+
* is spawned and nothing is written to disk, so `--dry-run` can call it safely.
|
|
4561
|
+
*/
|
|
4562
|
+
planActivation(options) {
|
|
4563
|
+
const {
|
|
4564
|
+
bundleName,
|
|
4565
|
+
workspace,
|
|
4566
|
+
scope,
|
|
4567
|
+
report,
|
|
4568
|
+
prompt,
|
|
4569
|
+
orchestrator,
|
|
4570
|
+
allowAddons,
|
|
4571
|
+
background,
|
|
4572
|
+
teams,
|
|
4573
|
+
pluginDir,
|
|
4574
|
+
manifestAvailable
|
|
4575
|
+
} = options;
|
|
4576
|
+
const command = report.command || {
|
|
4577
|
+
executable: "claude",
|
|
4578
|
+
prefixArgs: [],
|
|
4579
|
+
source: "path-executable"
|
|
4580
|
+
};
|
|
4581
|
+
const coordinatorName = _ClaudeLauncher.resolveCoordinatorName(orchestrator);
|
|
4582
|
+
const homePrefix = scope === "global" ? "~/" : "";
|
|
4583
|
+
const manifestRel = `${homePrefix}.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
|
|
4584
|
+
const coordinatorRel = `${homePrefix}.claude/agents/${coordinatorName}.md`;
|
|
4585
|
+
const addonPolicyText = allowAddons ? "Addon auto-installation is pre-authorized for this session." : `Before installing any recommended addon, explain the requirement to the user and request explicit confirmation to run: agents add <addon> -t claude ${scope === "global" ? "-g " : ""}-y.`;
|
|
4586
|
+
const deploymentNoteText = `Deployment note: this bundle is already projected natively for this workspace - roles in "${homePrefix}.claude/agents/", skills in "${homePrefix}.claude/skills/" and rules in "${homePrefix}.claude/rules/". The single source of truth stays the canonical store under "${homePrefix}.agents/" (ADR 0018): read the projections, edit the canonical files, never the copies.`;
|
|
4587
|
+
const delegationNoteText = `Delegation on this host works by description: in an ordinary Claude Code session you name a specialist and Claude picks the matching definition from "${homePrefix}.claude/agents/". This session was started with \`claude --agent ${coordinatorName}\`, so your own projected coordinator definition runs on the main thread and the real \`Agent(...)\` allowlist in its frontmatter is what bounds the specialists you may delegate to - send work only to those exact agent type names.`;
|
|
4588
|
+
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.";
|
|
4589
|
+
const manifestInstruction = manifestAvailable === false ? `Read your coordinator role definition at "${coordinatorRel}" before acting. Specialist roles are discoverable by name and description from the projected definitions in "${homePrefix}.claude/agents/" (the host-neutral team manifest is not present in this workspace yet - it is written by a \`--fanout cline\` install).` : `Read the Team Manifest at "${manifestRel}" and your coordinator role definition at "${coordinatorRel}" before acting.`;
|
|
4590
|
+
const bootstrapLines = [
|
|
4591
|
+
`You are coordinating the "${bundleName}" team in Agents United.`,
|
|
4592
|
+
manifestInstruction,
|
|
4593
|
+
delegationNoteText,
|
|
4594
|
+
"Use specialist roles only when necessary.",
|
|
4595
|
+
addonPolicyText,
|
|
4596
|
+
deploymentNoteText,
|
|
4597
|
+
taskText
|
|
4598
|
+
];
|
|
4599
|
+
if (teams === true) {
|
|
4600
|
+
bootstrapLines.push(
|
|
4601
|
+
`Agent teams are enabled for this session as an EXPERIMENTAL scaffold: spawn teammates from the projected agent types by name - the names you see in "${homePrefix}.claude/agents/" (specialists are projected with their "subagent-" prefix stripped, e.g. \`backend-architect\`). The scaffold is deliberately minimal: exactly one team per session, you are the fixed lead, there is no nested teams and no session resumption, and nothing about it is persisted to disk. Never make it load-bearing - if a teammate cannot be spawned, continue with ordinary delegated subagents using the Agent tool.`
|
|
4602
|
+
);
|
|
4603
|
+
}
|
|
4604
|
+
const bootstrapPrompt = bootstrapLines.join("\n\n");
|
|
4605
|
+
const env = {};
|
|
4606
|
+
if (teams === true) {
|
|
4607
|
+
env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1";
|
|
4608
|
+
}
|
|
4609
|
+
const argv = [...command.prefixArgs];
|
|
4610
|
+
argv.push("--agent");
|
|
4611
|
+
argv.push(coordinatorName);
|
|
4612
|
+
argv.push("--add-dir");
|
|
4613
|
+
argv.push(workspace);
|
|
4614
|
+
if (background === true) {
|
|
4615
|
+
argv.push("--bg");
|
|
4616
|
+
}
|
|
4617
|
+
if (pluginDir) {
|
|
4618
|
+
argv.push("--plugin-dir");
|
|
4619
|
+
argv.push(pluginDir);
|
|
4620
|
+
}
|
|
4621
|
+
argv.push(bootstrapPrompt);
|
|
4622
|
+
return {
|
|
4623
|
+
bundleName,
|
|
4624
|
+
scope,
|
|
4625
|
+
workspace,
|
|
4626
|
+
executable: command.executable,
|
|
4627
|
+
argv,
|
|
4628
|
+
bootstrapPrompt,
|
|
4629
|
+
env
|
|
4630
|
+
};
|
|
4631
|
+
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Launch the Claude Code session from the argv array. Mirrors `ClineLauncher.launch`: `shell: false`
|
|
4634
|
+
* (never `shell: true`), `stdio: 'inherit'`, and the plan's ephemeral env merged over `process.env`.
|
|
4635
|
+
*/
|
|
4636
|
+
async launch(plan) {
|
|
4637
|
+
const { spawn } = await import("child_process");
|
|
4638
|
+
const child = spawn(plan.executable, plan.argv, {
|
|
4639
|
+
cwd: plan.workspace,
|
|
4640
|
+
stdio: "inherit",
|
|
4641
|
+
shell: false,
|
|
4642
|
+
env: { ...process.env, ...plan.env }
|
|
4643
|
+
});
|
|
4644
|
+
await new Promise((resolve, reject) => {
|
|
4645
|
+
child.on("error", reject);
|
|
4646
|
+
child.on("exit", (code) => {
|
|
4647
|
+
if (code === 0 || code === null) {
|
|
4648
|
+
resolve();
|
|
4649
|
+
} else {
|
|
4650
|
+
resolve();
|
|
4651
|
+
}
|
|
4652
|
+
});
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
};
|
|
4656
|
+
|
|
2675
4657
|
// src/core/prerequisites.ts
|
|
2676
|
-
import
|
|
2677
|
-
import
|
|
2678
|
-
import
|
|
4658
|
+
import path16 from "path";
|
|
4659
|
+
import os6 from "os";
|
|
4660
|
+
import fs14 from "fs-extra";
|
|
2679
4661
|
|
|
2680
4662
|
// src/core/mcp-locations.ts
|
|
2681
|
-
import
|
|
2682
|
-
import
|
|
2683
|
-
import
|
|
4663
|
+
import path15 from "path";
|
|
4664
|
+
import os5 from "os";
|
|
4665
|
+
import fs13 from "fs-extra";
|
|
2684
4666
|
var McpLocationRegistry = class _McpLocationRegistry {
|
|
2685
4667
|
/**
|
|
2686
4668
|
* Declarative catalog of known MCP configuration files across AI tools, CLIs, extensions, and workspaces.
|
|
@@ -2692,7 +4674,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2692
4674
|
host: "gemini",
|
|
2693
4675
|
category: "cli",
|
|
2694
4676
|
label: "Google Antigravity & Gemini Global Config",
|
|
2695
|
-
resolvePath: (_, home) =>
|
|
4677
|
+
resolvePath: (_, home) => path15.join(home, ".gemini", "config", "mcp_config.json"),
|
|
2696
4678
|
isPrimary: true
|
|
2697
4679
|
},
|
|
2698
4680
|
{
|
|
@@ -2700,21 +4682,21 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2700
4682
|
host: "gemini",
|
|
2701
4683
|
category: "desktop",
|
|
2702
4684
|
label: "Google Antigravity System Config",
|
|
2703
|
-
resolvePath: (_, home) =>
|
|
4685
|
+
resolvePath: (_, home) => path15.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
2704
4686
|
},
|
|
2705
4687
|
{
|
|
2706
4688
|
id: "antigravity-workspace",
|
|
2707
4689
|
host: "gemini",
|
|
2708
4690
|
category: "workspace",
|
|
2709
4691
|
label: "Google Antigravity Local Workspace Config",
|
|
2710
|
-
resolvePath: (cwd) =>
|
|
4692
|
+
resolvePath: (cwd) => path15.join(cwd, ".gemini", "config", "mcp_config.json")
|
|
2711
4693
|
},
|
|
2712
4694
|
{
|
|
2713
4695
|
id: "gemini-appdata",
|
|
2714
4696
|
host: "gemini",
|
|
2715
4697
|
category: "cli",
|
|
2716
4698
|
label: "Google Gemini AppData Config",
|
|
2717
|
-
resolvePath: (_, __, appData) =>
|
|
4699
|
+
resolvePath: (_, __, appData) => path15.join(appData, "gemini", "config", "mcp_config.json")
|
|
2718
4700
|
},
|
|
2719
4701
|
// --- Cline CLI & VS Code Extension ---
|
|
2720
4702
|
{
|
|
@@ -2722,7 +4704,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2722
4704
|
host: "cline",
|
|
2723
4705
|
category: "cli",
|
|
2724
4706
|
label: "Cline CLI MCP Settings",
|
|
2725
|
-
resolvePath: (_, home) =>
|
|
4707
|
+
resolvePath: (_, home) => path15.join(home, ".cline", "data", "settings", "cline_mcp_settings.json"),
|
|
2726
4708
|
isPrimary: true
|
|
2727
4709
|
},
|
|
2728
4710
|
{
|
|
@@ -2730,42 +4712,42 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2730
4712
|
host: "cline",
|
|
2731
4713
|
category: "extension",
|
|
2732
4714
|
label: "Cline VS Code Extension Global Settings",
|
|
2733
|
-
resolvePath: (_, __, appData) =>
|
|
4715
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
2734
4716
|
},
|
|
2735
4717
|
{
|
|
2736
4718
|
id: "cline-insiders-extension-settings",
|
|
2737
4719
|
host: "cline",
|
|
2738
4720
|
category: "extension",
|
|
2739
4721
|
label: "Cline VS Code Insiders Extension Settings",
|
|
2740
|
-
resolvePath: (_, __, appData) =>
|
|
4722
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code - Insiders", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
2741
4723
|
},
|
|
2742
4724
|
{
|
|
2743
4725
|
id: "cline-legacy-settings",
|
|
2744
4726
|
host: "cline",
|
|
2745
4727
|
category: "cli",
|
|
2746
4728
|
label: "Cline Home Legacy Settings",
|
|
2747
|
-
resolvePath: (_, home) =>
|
|
4729
|
+
resolvePath: (_, home) => path15.join(home, ".cline", "settings", "cline_mcp_settings.json")
|
|
2748
4730
|
},
|
|
2749
4731
|
{
|
|
2750
4732
|
id: "cline-home-flat",
|
|
2751
4733
|
host: "cline",
|
|
2752
4734
|
category: "cli",
|
|
2753
4735
|
label: "Cline Home Flat Settings",
|
|
2754
|
-
resolvePath: (_, home) =>
|
|
4736
|
+
resolvePath: (_, home) => path15.join(home, "cline_mcp_settings.json")
|
|
2755
4737
|
},
|
|
2756
4738
|
{
|
|
2757
4739
|
id: "cline-workspace-flat",
|
|
2758
4740
|
host: "cline",
|
|
2759
4741
|
category: "workspace",
|
|
2760
4742
|
label: "Cline Workspace Settings",
|
|
2761
|
-
resolvePath: (cwd) =>
|
|
4743
|
+
resolvePath: (cwd) => path15.join(cwd, "cline_mcp_settings.json")
|
|
2762
4744
|
},
|
|
2763
4745
|
{
|
|
2764
4746
|
id: "cline-workspace-dot",
|
|
2765
4747
|
host: "cline",
|
|
2766
4748
|
category: "workspace",
|
|
2767
4749
|
label: "Cline Workspace .cline/mcp.json",
|
|
2768
|
-
resolvePath: (cwd) =>
|
|
4750
|
+
resolvePath: (cwd) => path15.join(cwd, ".cline", "mcp.json")
|
|
2769
4751
|
},
|
|
2770
4752
|
// --- Roo Code Extension ---
|
|
2771
4753
|
{
|
|
@@ -2773,7 +4755,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2773
4755
|
host: "cline",
|
|
2774
4756
|
category: "extension",
|
|
2775
4757
|
label: "Roo Code VS Code Extension Settings",
|
|
2776
|
-
resolvePath: (_, __, appData) =>
|
|
4758
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json")
|
|
2777
4759
|
},
|
|
2778
4760
|
// --- Cursor IDE / CLI ---
|
|
2779
4761
|
{
|
|
@@ -2781,7 +4763,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2781
4763
|
host: "cursor",
|
|
2782
4764
|
category: "workspace",
|
|
2783
4765
|
label: "Cursor Workspace Config",
|
|
2784
|
-
resolvePath: (cwd) =>
|
|
4766
|
+
resolvePath: (cwd) => path15.join(cwd, ".cursor", "mcp.json"),
|
|
2785
4767
|
isPrimary: true
|
|
2786
4768
|
},
|
|
2787
4769
|
{
|
|
@@ -2789,7 +4771,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2789
4771
|
host: "cursor",
|
|
2790
4772
|
category: "desktop",
|
|
2791
4773
|
label: "Cursor Global User Config",
|
|
2792
|
-
resolvePath: (_, home) =>
|
|
4774
|
+
resolvePath: (_, home) => path15.join(home, ".cursor", "mcp.json")
|
|
2793
4775
|
},
|
|
2794
4776
|
// --- Claude Code CLI & Claude Desktop ---
|
|
2795
4777
|
{
|
|
@@ -2797,7 +4779,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2797
4779
|
host: "claude",
|
|
2798
4780
|
category: "workspace",
|
|
2799
4781
|
label: "Claude Code Workspace Config",
|
|
2800
|
-
resolvePath: (cwd) =>
|
|
4782
|
+
resolvePath: (cwd) => path15.join(cwd, ".claude", "mcp.json"),
|
|
2801
4783
|
isPrimary: true
|
|
2802
4784
|
},
|
|
2803
4785
|
{
|
|
@@ -2805,28 +4787,28 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2805
4787
|
host: "claude",
|
|
2806
4788
|
category: "workspace",
|
|
2807
4789
|
label: "Claude Workspace claude.json",
|
|
2808
|
-
resolvePath: (cwd) =>
|
|
4790
|
+
resolvePath: (cwd) => path15.join(cwd, "claude.json")
|
|
2809
4791
|
},
|
|
2810
4792
|
{
|
|
2811
4793
|
id: "claude-global-dot",
|
|
2812
4794
|
host: "claude",
|
|
2813
4795
|
category: "cli",
|
|
2814
4796
|
label: "Claude Code Global Config",
|
|
2815
|
-
resolvePath: (_, home) =>
|
|
4797
|
+
resolvePath: (_, home) => path15.join(home, ".claude", "mcp.json")
|
|
2816
4798
|
},
|
|
2817
4799
|
{
|
|
2818
4800
|
id: "claude-global-flat",
|
|
2819
4801
|
host: "claude",
|
|
2820
4802
|
category: "cli",
|
|
2821
4803
|
label: "Claude Code Global claude.json",
|
|
2822
|
-
resolvePath: (_, home) =>
|
|
4804
|
+
resolvePath: (_, home) => path15.join(home, "claude.json")
|
|
2823
4805
|
},
|
|
2824
4806
|
{
|
|
2825
4807
|
id: "claude-desktop-global",
|
|
2826
4808
|
host: "claude",
|
|
2827
4809
|
category: "desktop",
|
|
2828
4810
|
label: "Claude Desktop Global Config",
|
|
2829
|
-
resolvePath: (_, __, appData) =>
|
|
4811
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Claude", "claude_desktop_config.json")
|
|
2830
4812
|
},
|
|
2831
4813
|
// --- Windsurf IDE ---
|
|
2832
4814
|
{
|
|
@@ -2834,7 +4816,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2834
4816
|
host: "windsurf",
|
|
2835
4817
|
category: "desktop",
|
|
2836
4818
|
label: "Windsurf Global Config",
|
|
2837
|
-
resolvePath: (_, home) =>
|
|
4819
|
+
resolvePath: (_, home) => path15.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2838
4820
|
isPrimary: true
|
|
2839
4821
|
},
|
|
2840
4822
|
{
|
|
@@ -2842,7 +4824,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2842
4824
|
host: "windsurf",
|
|
2843
4825
|
category: "desktop",
|
|
2844
4826
|
label: "Windsurf AppData Config",
|
|
2845
|
-
resolvePath: (_, __, appData) =>
|
|
4827
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Windsurf", "mcp_config.json")
|
|
2846
4828
|
},
|
|
2847
4829
|
// --- OpenCode & Codex ---
|
|
2848
4830
|
{
|
|
@@ -2850,7 +4832,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2850
4832
|
host: "opencode",
|
|
2851
4833
|
category: "workspace",
|
|
2852
4834
|
label: "OpenCode Workspace Config",
|
|
2853
|
-
resolvePath: (cwd) =>
|
|
4835
|
+
resolvePath: (cwd) => path15.join(cwd, ".opencode", "mcp.json"),
|
|
2854
4836
|
isPrimary: true
|
|
2855
4837
|
},
|
|
2856
4838
|
{
|
|
@@ -2858,28 +4840,28 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2858
4840
|
host: "opencode",
|
|
2859
4841
|
category: "cli",
|
|
2860
4842
|
label: "OpenCode Global Config",
|
|
2861
|
-
resolvePath: (_, home) =>
|
|
4843
|
+
resolvePath: (_, home) => path15.join(home, ".config", "opencode", "mcp.json")
|
|
2862
4844
|
}
|
|
2863
4845
|
];
|
|
2864
4846
|
/**
|
|
2865
4847
|
* Discovers all MCP configuration files across the filesystem, inspecting existence and parsing server definitions.
|
|
2866
4848
|
*/
|
|
2867
4849
|
static async discoverAll(cwd = process.cwd()) {
|
|
2868
|
-
const homeDir =
|
|
2869
|
-
const appData = process.env.APPDATA || (process.platform === "darwin" ?
|
|
4850
|
+
const homeDir = os5.homedir();
|
|
4851
|
+
const appData = process.env.APPDATA || (process.platform === "darwin" ? path15.join(homeDir, "Library", "Application Support") : path15.join(homeDir, ".config"));
|
|
2870
4852
|
const results = [];
|
|
2871
4853
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
2872
4854
|
for (const desc of _McpLocationRegistry.LOCATIONS) {
|
|
2873
|
-
const resolved =
|
|
4855
|
+
const resolved = path15.normalize(desc.resolvePath(cwd, homeDir, appData));
|
|
2874
4856
|
const key = resolved.toLowerCase();
|
|
2875
4857
|
if (seenPaths.has(key)) continue;
|
|
2876
4858
|
seenPaths.add(key);
|
|
2877
|
-
const exists = await
|
|
4859
|
+
const exists = await fs13.pathExists(resolved);
|
|
2878
4860
|
let serverCount = 0;
|
|
2879
4861
|
let servers;
|
|
2880
4862
|
if (exists) {
|
|
2881
4863
|
try {
|
|
2882
|
-
const config = await
|
|
4864
|
+
const config = await fs13.readJson(resolved);
|
|
2883
4865
|
const rawServers = config?.mcpServers || config?.servers || config;
|
|
2884
4866
|
if (rawServers && typeof rawServers === "object") {
|
|
2885
4867
|
servers = rawServers;
|
|
@@ -2914,14 +4896,14 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
2914
4896
|
* Resolves the primary target write path for a given host runtime.
|
|
2915
4897
|
*/
|
|
2916
4898
|
static getPrimaryWritePath(host, cwd = process.cwd()) {
|
|
2917
|
-
const homeDir =
|
|
2918
|
-
const appData = process.env.APPDATA || (process.platform === "darwin" ?
|
|
4899
|
+
const homeDir = os5.homedir();
|
|
4900
|
+
const appData = process.env.APPDATA || (process.platform === "darwin" ? path15.join(homeDir, "Library", "Application Support") : path15.join(homeDir, ".config"));
|
|
2919
4901
|
const targetHost = host === "agents" ? "gemini" : host;
|
|
2920
4902
|
const desc = _McpLocationRegistry.LOCATIONS.find((l) => l.host === targetHost && l.isPrimary);
|
|
2921
4903
|
if (desc) {
|
|
2922
|
-
return
|
|
4904
|
+
return path15.normalize(desc.resolvePath(cwd, homeDir, appData));
|
|
2923
4905
|
}
|
|
2924
|
-
return
|
|
4906
|
+
return path15.normalize(path15.join(cwd, ".cursor", "mcp.json"));
|
|
2925
4907
|
}
|
|
2926
4908
|
};
|
|
2927
4909
|
|
|
@@ -3036,21 +5018,21 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3036
5018
|
});
|
|
3037
5019
|
}
|
|
3038
5020
|
}
|
|
3039
|
-
const homeDir =
|
|
5021
|
+
const homeDir = os6.homedir();
|
|
3040
5022
|
const dirCandidates = [
|
|
3041
|
-
|
|
3042
|
-
|
|
5023
|
+
path16.join(cwd, ".gemini", "antigravity", "mcp"),
|
|
5024
|
+
path16.join(homeDir, ".gemini", "antigravity", "mcp")
|
|
3043
5025
|
];
|
|
3044
5026
|
for (const dir of dirCandidates) {
|
|
3045
|
-
if (await
|
|
3046
|
-
const entries = await
|
|
5027
|
+
if (await fs14.pathExists(dir)) {
|
|
5028
|
+
const entries = await fs14.readdir(dir).catch(() => []);
|
|
3047
5029
|
for (const entry of entries) {
|
|
3048
5030
|
if (_PrerequisiteChecker.matchesMcpServer(mcpName, entry, {})) {
|
|
3049
5031
|
foundInConfigs.push({
|
|
3050
5032
|
id: "antigravity-dir",
|
|
3051
5033
|
host: "gemini",
|
|
3052
5034
|
label: "Google Antigravity MCP Directory",
|
|
3053
|
-
path:
|
|
5035
|
+
path: path16.join(dir, entry),
|
|
3054
5036
|
disabled: false,
|
|
3055
5037
|
serverKey: entry
|
|
3056
5038
|
});
|
|
@@ -3075,14 +5057,14 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3075
5057
|
* Checks if an npm package is installed in node_modules or declared in package.json
|
|
3076
5058
|
*/
|
|
3077
5059
|
static async isPackageInstalled(pkgName, cwd = process.cwd()) {
|
|
3078
|
-
const modulePath =
|
|
3079
|
-
if (await
|
|
5060
|
+
const modulePath = path16.join(cwd, "node_modules", ...pkgName.split("/"));
|
|
5061
|
+
if (await fs14.pathExists(modulePath)) {
|
|
3080
5062
|
return { installed: true, location: modulePath };
|
|
3081
5063
|
}
|
|
3082
|
-
const pkgJsonPath =
|
|
3083
|
-
if (await
|
|
5064
|
+
const pkgJsonPath = path16.join(cwd, "package.json");
|
|
5065
|
+
if (await fs14.pathExists(pkgJsonPath)) {
|
|
3084
5066
|
try {
|
|
3085
|
-
const pkg = await
|
|
5067
|
+
const pkg = await fs14.readJson(pkgJsonPath);
|
|
3086
5068
|
const deps = {
|
|
3087
5069
|
...pkg.dependencies,
|
|
3088
5070
|
...pkg.devDependencies,
|
|
@@ -3105,20 +5087,20 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3105
5087
|
return { set: true, source: "process.env" };
|
|
3106
5088
|
}
|
|
3107
5089
|
const envFiles = [
|
|
3108
|
-
|
|
3109
|
-
|
|
5090
|
+
path16.join(cwd, ".env.local"),
|
|
5091
|
+
path16.join(cwd, ".env")
|
|
3110
5092
|
];
|
|
3111
5093
|
for (const envFile of envFiles) {
|
|
3112
|
-
if (await
|
|
5094
|
+
if (await fs14.pathExists(envFile)) {
|
|
3113
5095
|
try {
|
|
3114
|
-
const content = await
|
|
5096
|
+
const content = await fs14.readFile(envFile, "utf8");
|
|
3115
5097
|
const lines = content.split("\n");
|
|
3116
5098
|
for (const line of lines) {
|
|
3117
5099
|
const trimmed = line.trim();
|
|
3118
5100
|
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
3119
5101
|
const [k, ...v] = trimmed.split("=");
|
|
3120
5102
|
if (k.trim() === varName && v.join("=").trim() !== "") {
|
|
3121
|
-
return { set: true, source:
|
|
5103
|
+
return { set: true, source: path16.basename(envFile) };
|
|
3122
5104
|
}
|
|
3123
5105
|
}
|
|
3124
5106
|
} catch {
|
|
@@ -3324,11 +5306,11 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3324
5306
|
* activating disabled servers and appending missing definitions.
|
|
3325
5307
|
*/
|
|
3326
5308
|
static async autoConfigureConfigFile(targetFilePath, mcpNames, mode = "operational", envVars = {}) {
|
|
3327
|
-
await
|
|
5309
|
+
await fs14.ensureDir(path16.dirname(targetFilePath));
|
|
3328
5310
|
let existingConfig = {};
|
|
3329
|
-
if (await
|
|
5311
|
+
if (await fs14.pathExists(targetFilePath)) {
|
|
3330
5312
|
try {
|
|
3331
|
-
existingConfig = await
|
|
5313
|
+
existingConfig = await fs14.readJson(targetFilePath);
|
|
3332
5314
|
} catch {
|
|
3333
5315
|
existingConfig = {};
|
|
3334
5316
|
}
|
|
@@ -3360,7 +5342,7 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3360
5342
|
} else {
|
|
3361
5343
|
existingConfig.mcpServers = existingServers;
|
|
3362
5344
|
}
|
|
3363
|
-
await
|
|
5345
|
+
await fs14.writeJson(targetFilePath, existingConfig, { spaces: 2 });
|
|
3364
5346
|
return {
|
|
3365
5347
|
success: true,
|
|
3366
5348
|
targetFile: targetFilePath,
|
|
@@ -3405,7 +5387,7 @@ var updater = new UpdateEngine(registry, scanner, installer);
|
|
|
3405
5387
|
function detectWorkspaceHosts(cwd = process.cwd()) {
|
|
3406
5388
|
const detected = [];
|
|
3407
5389
|
for (const host of Object.values(HOST_REGISTRY)) {
|
|
3408
|
-
const found = host.detectionMarkers.some((marker) =>
|
|
5390
|
+
const found = host.detectionMarkers.some((marker) => fs15.pathExistsSync(path17.join(cwd, marker)));
|
|
3409
5391
|
if (found) detected.push(host.id);
|
|
3410
5392
|
}
|
|
3411
5393
|
return detected;
|
|
@@ -3633,10 +5615,10 @@ var BUNDLE_DISPLAY_NAMES = {
|
|
|
3633
5615
|
},
|
|
3634
5616
|
"full": {
|
|
3635
5617
|
title: "All-in-One Autonomous Department",
|
|
3636
|
-
summary: "Complete suite with all 7 team leads, 38 agents, and all skills & workflows"
|
|
5618
|
+
summary: "Complete suite with all 7 team leads, 38 agents, and all 160 modular skills & workflows"
|
|
3637
5619
|
}
|
|
3638
5620
|
};
|
|
3639
|
-
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, cursor,
|
|
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 = {}) => {
|
|
3640
5622
|
intro(pc.cyan("Agents United \u2014 AI Agent Ecosystem"));
|
|
3641
5623
|
let identifier = targetIdentifier;
|
|
3642
5624
|
let scope = options.global ? "global" : "project";
|
|
@@ -3670,6 +5652,14 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
3670
5652
|
);
|
|
3671
5653
|
}
|
|
3672
5654
|
fanout = Array.from(/* @__PURE__ */ new Set([...fanout, ...parsedFanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h].projectionCapable)]));
|
|
5655
|
+
const underDevFanout = Array.from(new Set(parsedFanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h].status === "under-development")));
|
|
5656
|
+
if (underDevFanout.length > 0) {
|
|
5657
|
+
note(
|
|
5658
|
+
`Refusing --fanout host(s): ${underDevFanout.join(", ")} \u2014 they are listed as Under Development and unavailable. Supported --fanout ids: ${SUPPORTED_HOST_IDS.filter((h) => HOST_REGISTRY[h].projectionCapable).join(", ")}. Drop ${underDevFanout.join(", ")} from --fanout and run again.`,
|
|
5659
|
+
"Host unavailable"
|
|
5660
|
+
);
|
|
5661
|
+
process.exit(1);
|
|
5662
|
+
}
|
|
3673
5663
|
}
|
|
3674
5664
|
const isInteractive = process.stdout.isTTY && !options.yes;
|
|
3675
5665
|
if (isInteractive && !options.global && !options.copy && !options.symlink && options.target === "agents") {
|
|
@@ -3680,6 +5670,7 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
3680
5670
|
"Workspace Environment Discovery"
|
|
3681
5671
|
);
|
|
3682
5672
|
}
|
|
5673
|
+
note(hostAvailabilityNotice(), "Host availability");
|
|
3683
5674
|
const hostSelection = await multiselect({
|
|
3684
5675
|
message: "1. Which AI tools should we configure agent teams & personas for?",
|
|
3685
5676
|
options: [
|
|
@@ -3693,33 +5684,18 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
3693
5684
|
label: HOST_REGISTRY.claude.label,
|
|
3694
5685
|
hint: detectedHosts.includes("claude") ? "found in this project" : "orchestrator & subagent personas for Claude Code"
|
|
3695
5686
|
},
|
|
3696
|
-
{
|
|
3697
|
-
value: "cursor",
|
|
3698
|
-
label: HOST_REGISTRY.cursor.label,
|
|
3699
|
-
hint: detectedHosts.includes("cursor") ? "found in this project" : "orchestrator & subagent personas for Cursor IDE"
|
|
3700
|
-
},
|
|
3701
5687
|
{
|
|
3702
5688
|
value: "cline",
|
|
3703
5689
|
label: HOST_REGISTRY.cline.label,
|
|
3704
5690
|
hint: detectedHosts.includes("cline") ? "found in this project" : "configured agents, skills, rules, workflows & team manifests for Cline"
|
|
3705
5691
|
},
|
|
3706
|
-
{
|
|
3707
|
-
value: "opencode",
|
|
3708
|
-
label: HOST_REGISTRY.opencode.label,
|
|
3709
|
-
hint: detectedHosts.includes("opencode") ? "found in this project" : "orchestrator & subagent personas for OpenCode"
|
|
3710
|
-
},
|
|
3711
|
-
{
|
|
3712
|
-
value: "codex",
|
|
3713
|
-
label: HOST_REGISTRY.codex.label,
|
|
3714
|
-
hint: detectedHosts.includes("codex") ? "found in this project" : "root index linking orchestrators, subagents & skills for Codex, Copilot, Aider & Zed"
|
|
3715
|
-
},
|
|
3716
5692
|
{
|
|
3717
5693
|
value: "gemini",
|
|
3718
5694
|
label: HOST_REGISTRY.gemini.label,
|
|
3719
5695
|
hint: detectedHosts.includes("gemini") ? "found in this project" : "older Antigravity 1.0 / Gemini folder"
|
|
3720
5696
|
}
|
|
3721
5697
|
],
|
|
3722
|
-
initialValues: detectedHosts.length > 0 ? detectedHosts : ["agents"],
|
|
5698
|
+
initialValues: detectedHosts.length > 0 ? detectedHosts.filter((h) => HOST_REGISTRY[h]?.status === "supported") : ["agents"],
|
|
3723
5699
|
required: true
|
|
3724
5700
|
});
|
|
3725
5701
|
if (Array.isArray(hostSelection) && hostSelection.length > 0) {
|
|
@@ -4024,6 +6000,10 @@ To bypass this gate and install draft assets anyway, pass: --allow-under-constru
|
|
|
4024
6000
|
method,
|
|
4025
6001
|
hosts,
|
|
4026
6002
|
fanout,
|
|
6003
|
+
// Plan 016 decision 13 — opt-in Claude plugin lane (distribution-only; ignored unless the
|
|
6004
|
+
// claude lane runs). `--plugin` opts in, `--no-plugin` opts out explicitly, and an omitted
|
|
6005
|
+
// flag inherits the recorded choice so `agents update` cannot prune the package.
|
|
6006
|
+
pluginLane: typeof options.plugin === "boolean" ? options.plugin : void 0,
|
|
4027
6007
|
mode: executionMode,
|
|
4028
6008
|
allowMissingPrereqs: options.allowMissingPrereqs,
|
|
4029
6009
|
yes: options.yes,
|
|
@@ -4147,6 +6127,14 @@ rules & workflows run: agents update ${identifier} --fanout cline
|
|
|
4147
6127
|
"Adaptive Tooling"
|
|
4148
6128
|
);
|
|
4149
6129
|
}
|
|
6130
|
+
if (!options.dryRun) {
|
|
6131
|
+
note(
|
|
6132
|
+
pc.yellow(
|
|
6133
|
+
"New skills are discovered at session start. To pick them up now:\n Claude Code ..... user types /reload-skills (#58733)\n Antigravity CLI . user types /skills reload\n Antigravity App . restart the app, or use @<skill_name>\n Cline ........... agents read .cline/skills/ directly\nSlash commands only fire when YOU type them; agents can read the file."
|
|
6134
|
+
),
|
|
6135
|
+
"Pick up new skills"
|
|
6136
|
+
);
|
|
6137
|
+
}
|
|
4150
6138
|
outro(pc.green(`\u2714 Installed "${identifier}" successfully!`));
|
|
4151
6139
|
} catch (err) {
|
|
4152
6140
|
s.stop(pc.red("Failed resolution"));
|
|
@@ -4214,10 +6202,25 @@ cli.command("remove [identifier]", "Remove an installed bundle, agent, skill, or
|
|
|
4214
6202
|
});
|
|
4215
6203
|
s.stop(`Uninstall processed`);
|
|
4216
6204
|
if (options.dryRun) {
|
|
4217
|
-
outro(pc.yellow(
|
|
6205
|
+
outro(pc.yellow(
|
|
6206
|
+
`[DRY RUN] Would delete ${result.removed.length} and keep ${result.kept.length}` + (result.kept.length > 0 ? ` (still owned by: ${result.retainedOwners.join(", ")})` : "") + ` \u2014 ${result.targetDirs.join(", ")}`
|
|
6207
|
+
));
|
|
4218
6208
|
return;
|
|
4219
6209
|
}
|
|
4220
|
-
|
|
6210
|
+
if (result.kept.length > 0) {
|
|
6211
|
+
log.info(`${result.removed.length} deleted \xB7 ${result.kept.length} kept \u2014 still owned by: ${result.retainedOwners.join(", ")}`);
|
|
6212
|
+
}
|
|
6213
|
+
if (result.removed.length > 0) {
|
|
6214
|
+
const sample = result.removed.slice(0, 3).join(", ");
|
|
6215
|
+
const more = result.removed.length > 3 ? `, \u2026 +${result.removed.length - 3} more` : "";
|
|
6216
|
+
log.info(`deleted: ${sample}${more}`);
|
|
6217
|
+
}
|
|
6218
|
+
if (result.staleRecords.length > 0) {
|
|
6219
|
+
log.info(`${result.staleRecords.length} stale record(s) cleaned \u2014 bookkeeping only, those files were already gone`);
|
|
6220
|
+
}
|
|
6221
|
+
outro(pc.green(
|
|
6222
|
+
result.removed.length === 0 && result.kept.length > 0 ? `\u2714 Removed "${identifier}" from this workspace \u2014 nothing was deleted, ${result.kept.length} co-owned assets kept` : `\u2714 Removed "${identifier}" from this workspace \u2014 ${result.removed.length} deleted, ${result.kept.length} kept`
|
|
6223
|
+
));
|
|
4221
6224
|
} catch (err) {
|
|
4222
6225
|
s.stop(pc.red("Uninstall failed"));
|
|
4223
6226
|
outro(pc.red(`Error: ${err.message}`));
|
|
@@ -4239,6 +6242,16 @@ cli.command("update [identifier]", "Update installed packages to upstream regist
|
|
|
4239
6242
|
"Invalid fanout"
|
|
4240
6243
|
);
|
|
4241
6244
|
}
|
|
6245
|
+
const updateUnderDev = Array.from(new Set(parsedFanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h].status === "under-development")));
|
|
6246
|
+
if (updateUnderDev.length > 0) {
|
|
6247
|
+
note(
|
|
6248
|
+
pc.yellow(
|
|
6249
|
+
`Refusing --fanout host(s): ${updateUnderDev.join(", ")} \u2014 they are listed as Under Development and unavailable. Supported --fanout ids: ${SUPPORTED_HOST_IDS.filter((h) => HOST_REGISTRY[h].projectionCapable).join(", ")}. Drop ${updateUnderDev.join(", ")} from --fanout and run again.`
|
|
6250
|
+
),
|
|
6251
|
+
"Host unavailable"
|
|
6252
|
+
);
|
|
6253
|
+
process.exit(1);
|
|
6254
|
+
}
|
|
4242
6255
|
updateFanout = parsedFanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h].projectionCapable);
|
|
4243
6256
|
}
|
|
4244
6257
|
const s = spinner();
|
|
@@ -4361,7 +6374,7 @@ Updates Available: ${report.outdatedCount > 0 ? pc.yellow(pc.bold(`${report.outd
|
|
|
4361
6374
|
const unprojected = [];
|
|
4362
6375
|
for (const rec of result.updated) {
|
|
4363
6376
|
try {
|
|
4364
|
-
const lf = await
|
|
6377
|
+
const lf = await fs15.readJson(path17.join(rec.targetDir, "agents-united.json"));
|
|
4365
6378
|
if (!lf.fanout || lf.fanout.length === 0) unprojected.push(rec.name);
|
|
4366
6379
|
} catch {
|
|
4367
6380
|
}
|
|
@@ -4400,7 +6413,7 @@ function renderBundleDetailTree(bundle) {
|
|
|
4400
6413
|
if (bundle.name === "full") {
|
|
4401
6414
|
const lines2 = [];
|
|
4402
6415
|
lines2.push(`\u{1F4E6} ${pc.bold(pc.green("full"))} ${pc.cyan("(Universal Autonomous Department)")} ${pc.green("\u2B50 [Recommended]")}`);
|
|
4403
|
-
lines2.push(`\u2502 ${pc.white("Complete suite with all 7 lead orchestrators, 38 subagents,
|
|
6416
|
+
lines2.push(`\u2502 ${pc.white("Complete suite with all 7 lead orchestrators, 38 subagents, and 160 modular skills (91 domain skills + 69 workflow playbooks).")}`);
|
|
4404
6417
|
lines2.push(`\u2502`);
|
|
4405
6418
|
lines2.push(`\u251C\u2500\u2500 \u{1F916} Lead Orchestrators (7 department domains):`);
|
|
4406
6419
|
lines2.push(`\u2502 \u251C\u2500\u2500 \u{1F6E0}\uFE0F ${pc.blue("orchestrator-engineering")} ${pc.dim("(Software Engineering & Delivery)")}`);
|
|
@@ -4411,8 +6424,8 @@ function renderBundleDetailTree(bundle) {
|
|
|
4411
6424
|
lines2.push(`\u2502 \u251C\u2500\u2500 \u{1F52C} ${pc.blue("orchestrator-research")} ${pc.dim("(Deep Technical Research)")}`);
|
|
4412
6425
|
lines2.push(`\u2502 \u2514\u2500\u2500 \u{1F4BC} ${pc.blue("orchestrator-business")} ${pc.dim("(Business Strategy & Economics)")}`);
|
|
4413
6426
|
lines2.push(`\u251C\u2500\u2500 \u{1F916} Specialized Sub-Agents: ${pc.blue("38 worker agents across all 8 departments")}`);
|
|
4414
|
-
lines2.push(`\u251C\u2500\u2500 \u26A1 Skills: ${pc.yellow("
|
|
4415
|
-
lines2.push(`\u251C\u2500\u2500 \u{1F504} Workflows: ${pc.magenta("
|
|
6427
|
+
lines2.push(`\u251C\u2500\u2500 \u26A1 Skills: ${pc.yellow("91 modular domain skills & runbooks")}`);
|
|
6428
|
+
lines2.push(`\u251C\u2500\u2500 \u{1F504} Workflows: ${pc.magenta("69 guided multi-step workflow playbooks")}`);
|
|
4416
6429
|
lines2.push(`\u251C\u2500\u2500 \u{1F50C} Prerequisites: ${pc.dim("None (Self-contained universal suite)")}`);
|
|
4417
6430
|
lines2.push(`\u2514\u2500\u2500 \u{1F4A1} Execution Modes: ${pc.green("Operational")}`);
|
|
4418
6431
|
return lines2.join("\n");
|
|
@@ -4457,27 +6470,34 @@ function renderBundleDetailTree(bundle) {
|
|
|
4457
6470
|
} else {
|
|
4458
6471
|
lines.push(`\u251C\u2500\u2500 \u{1F916} Sub-agents: ${pc.dim("None")}`);
|
|
4459
6472
|
}
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
6473
|
+
const domainSkills = (bundle.skills || []).filter((s) => !s.startsWith("workflow-"));
|
|
6474
|
+
const workflowSkills = [
|
|
6475
|
+
...(bundle.skills || []).filter((s) => s.startsWith("workflow-")),
|
|
6476
|
+
...bundle.workflows || []
|
|
6477
|
+
];
|
|
6478
|
+
if (domainSkills.length > 0) {
|
|
6479
|
+
lines.push(`\u251C\u2500\u2500 \u26A1 Skills (${domainSkills.length}):`);
|
|
6480
|
+
if (domainSkills.length <= 5) {
|
|
6481
|
+
lines.push(`\u2502 \u2514\u2500\u2500 ${pc.yellow(domainSkills.join(", "))}`);
|
|
4464
6482
|
} else {
|
|
4465
|
-
lines.push(`\u2502 \u251C\u2500\u2500 ${pc.yellow(
|
|
4466
|
-
lines.push(`\u2502 \u2514\u2500\u2500 ${pc.dim(`(+${
|
|
6483
|
+
lines.push(`\u2502 \u251C\u2500\u2500 ${pc.yellow(domainSkills.slice(0, 5).join(", "))}`);
|
|
6484
|
+
lines.push(`\u2502 \u2514\u2500\u2500 ${pc.dim(`(+${domainSkills.length - 5} more: ${domainSkills.slice(5).join(", ")})`)}`);
|
|
4467
6485
|
}
|
|
4468
|
-
} else {
|
|
6486
|
+
} else if (!bundle.parentBundle && workflowSkills.length === 0) {
|
|
6487
|
+
lines.push(`\u251C\u2500\u2500 \u26A1 Skills: ${pc.dim("None")}`);
|
|
6488
|
+
} else if (bundle.parentBundle && domainSkills.length === 0) {
|
|
4469
6489
|
lines.push(`\u251C\u2500\u2500 \u26A1 Skills: ${pc.dim("Inherited from base bundle")}`);
|
|
4470
6490
|
}
|
|
4471
|
-
if (
|
|
4472
|
-
const wfNames =
|
|
4473
|
-
lines.push(`\u251C\u2500\u2500 \u{1F504} Workflows (${
|
|
6491
|
+
if (workflowSkills.length > 0) {
|
|
6492
|
+
const wfNames = workflowSkills.map((w) => w.replace(/^workflow-/, "").replace(/\.md$/, ""));
|
|
6493
|
+
lines.push(`\u251C\u2500\u2500 \u{1F504} Workflows (${workflowSkills.length}):`);
|
|
4474
6494
|
if (wfNames.length <= 4) {
|
|
4475
6495
|
lines.push(`\u2502 \u2514\u2500\u2500 ${pc.magenta(wfNames.join(", "))}`);
|
|
4476
6496
|
} else {
|
|
4477
6497
|
lines.push(`\u2502 \u251C\u2500\u2500 ${pc.magenta(wfNames.slice(0, 4).join(", "))}`);
|
|
4478
6498
|
lines.push(`\u2502 \u2514\u2500\u2500 ${pc.dim(`(+${wfNames.length - 4} more: ${wfNames.slice(4).join(", ")})`)}`);
|
|
4479
6499
|
}
|
|
4480
|
-
} else {
|
|
6500
|
+
} else if (bundle.parentBundle) {
|
|
4481
6501
|
lines.push(`\u251C\u2500\u2500 \u{1F504} Workflows: ${pc.dim("Inherited from base bundle")}`);
|
|
4482
6502
|
}
|
|
4483
6503
|
if (bundle.prerequisites) {
|
|
@@ -4569,15 +6589,20 @@ ${pc.bold(pc.magenta(header))} ${pc.dim(`(${items.length} bundle${items.length >
|
|
|
4569
6589
|
const displaySubs = subNames.length > 3 ? `${subNames.slice(0, 3).join(", ")} (+${subNames.length - 3} more)` : subNames.join(", ");
|
|
4570
6590
|
console.log(`${subIndent}\u251C\u2500\u2500 \u{1F916} Sub-agents: ${pc.blue(displaySubs)}`);
|
|
4571
6591
|
}
|
|
4572
|
-
|
|
4573
|
-
|
|
6592
|
+
const domainSkills = (b.skills || []).filter((s) => !s.startsWith("workflow-"));
|
|
6593
|
+
const workflowSkills = [
|
|
6594
|
+
...(b.skills || []).filter((s) => s.startsWith("workflow-")),
|
|
6595
|
+
...b.workflows || []
|
|
6596
|
+
];
|
|
6597
|
+
if (domainSkills.length > 0) {
|
|
6598
|
+
const displaySkills = domainSkills.length > 3 ? `${domainSkills.slice(0, 3).join(", ")} (+${domainSkills.length - 3} more)` : domainSkills.join(", ");
|
|
4574
6599
|
console.log(`${subIndent}\u251C\u2500\u2500 \u26A1 Skills: ${pc.yellow(displaySkills)}`);
|
|
4575
6600
|
}
|
|
4576
|
-
if (
|
|
4577
|
-
const wfNames =
|
|
6601
|
+
if (workflowSkills.length > 0) {
|
|
6602
|
+
const wfNames = workflowSkills.map((w) => w.replace(/^workflow-/, "").replace(/\.md$/, ""));
|
|
4578
6603
|
const displayWfs = wfNames.length > 3 ? `${wfNames.slice(0, 3).join(", ")} (+${wfNames.length - 3} more)` : wfNames.join(", ");
|
|
4579
6604
|
console.log(`${subIndent}\u2514\u2500\u2500 \u{1F504} Workflows: ${pc.magenta(displayWfs)}`);
|
|
4580
|
-
} else {
|
|
6605
|
+
} else if (b.parentBundle) {
|
|
4581
6606
|
console.log(`${subIndent}\u2514\u2500\u2500 \u{1F504} Workflows: ${pc.dim("Inherited from parent")}`);
|
|
4582
6607
|
}
|
|
4583
6608
|
if (!isLastBundle) {
|
|
@@ -4611,12 +6636,17 @@ ${pc.bold(pc.cyan("\u{1F3E2} Organization Bundles (Experimental / Cross-Functio
|
|
|
4611
6636
|
const displaySubs = subNames.length > 3 ? `${subNames.slice(0, 3).join(", ")} (+${subNames.length - 3} more)` : subNames.join(", ");
|
|
4612
6637
|
console.log(`${subIndent}\u251C\u2500\u2500 \u{1F916} Sub-agents: ${pc.blue(displaySubs)}`);
|
|
4613
6638
|
}
|
|
4614
|
-
|
|
4615
|
-
|
|
6639
|
+
const orgDomainSkills = (b.skills || []).filter((s) => !s.startsWith("workflow-"));
|
|
6640
|
+
const orgWorkflowSkills = [
|
|
6641
|
+
...(b.skills || []).filter((s) => s.startsWith("workflow-")),
|
|
6642
|
+
...b.workflows || []
|
|
6643
|
+
];
|
|
6644
|
+
if (orgDomainSkills.length > 0) {
|
|
6645
|
+
const displaySkills = orgDomainSkills.length > 3 ? `${orgDomainSkills.slice(0, 3).join(", ")} (+${orgDomainSkills.length - 3} more)` : orgDomainSkills.join(", ");
|
|
4616
6646
|
console.log(`${subIndent}\u251C\u2500\u2500 \u26A1 Skills: ${pc.yellow(displaySkills)}`);
|
|
4617
6647
|
}
|
|
4618
|
-
if (
|
|
4619
|
-
const wfNames =
|
|
6648
|
+
if (orgWorkflowSkills.length > 0) {
|
|
6649
|
+
const wfNames = orgWorkflowSkills.map((w) => w.replace(/^workflow-/, "").replace(/\.md$/, ""));
|
|
4620
6650
|
const displayWfs = wfNames.length > 3 ? `${wfNames.slice(0, 3).join(", ")} (+${wfNames.length - 3} more)` : wfNames.join(", ");
|
|
4621
6651
|
console.log(`${subIndent}\u251C\u2500\u2500 \u{1F504} Workflows: ${pc.magenta(displayWfs)}`);
|
|
4622
6652
|
}
|
|
@@ -4783,6 +6813,12 @@ Planned Capabilities:
|
|
|
4783
6813
|
method: "symlink"
|
|
4784
6814
|
});
|
|
4785
6815
|
installSpinner.stop(pc.green(`\u2714 Successfully installed ${bundle.name}!`));
|
|
6816
|
+
note(
|
|
6817
|
+
pc.yellow(
|
|
6818
|
+
"New skills are discovered at session start. To pick them up now:\n Claude Code ..... user types /reload-skills (#58733)\n Antigravity CLI . user types /skills reload\n Antigravity App . restart the app, or use @<skill_name>\n Cline ........... agents read .cline/skills/ directly\nSlash commands only fire when YOU type them; agents can read the file."
|
|
6819
|
+
),
|
|
6820
|
+
"Pick up new skills"
|
|
6821
|
+
);
|
|
4786
6822
|
note(
|
|
4787
6823
|
formatInstallationSummary({
|
|
4788
6824
|
bundleName: result.installed.targetBundle || bundle.name,
|
|
@@ -5078,11 +7114,19 @@ cli.command("init", "Initialize project workspace directory and install recommen
|
|
|
5078
7114
|
process.exit(1);
|
|
5079
7115
|
}
|
|
5080
7116
|
});
|
|
5081
|
-
cli.command("start <bundle> [prompt]", "Start an installed bundle team in its host runtime (e.g. Cline)").option("--host <host>", "Host runtime (default: auto-detect from lockfile fanout)").option("-g, --global", "Select global installation").option("--team <name>", "Override generated team name").option("--allow-addons", "Pre-authorize recommended addon installations for this session").option("--headless", "Run non-interactively without interactive TUI").option("--dry-run", "Print activation resolution and argv summary without launching").action(async (bundle, prompt, options = {}) => {
|
|
7117
|
+
cli.command("start <bundle> [prompt]", "Start an installed bundle team in its host runtime (e.g. Cline, Claude Code)").option("--host <host>", "Host runtime (default: auto-detect from lockfile fanout)").option("-g, --global", "Select global installation").option("--team <name>", "Override generated team name").option("--allow-addons", "Pre-authorize recommended addon installations for this session").option("--headless", "Run non-interactively without interactive TUI").option("--bg", "Run the Claude Code session in the background (claude host only)").option("--teams", "Force the experimental agent-teams scaffold on (claude host only). Default: ON for organization-tier (Tier 2) bundles, OFF for domain-tier (Tier 1) bundles \u2014 pass --no-teams to opt out. Ephemeral env only, nothing persisted").option("--plugin", "Pass --plugin-dir with the bundle plugin root (claude host only)").option("--dry-run", "Print activation resolution and argv summary without launching").action(async (bundle, prompt, options = {}) => {
|
|
5082
7118
|
intro(pc.cyan("Agents United \u2014 Runtime Activation"));
|
|
5083
7119
|
const launcher = new ClineLauncher();
|
|
5084
7120
|
const probe = new ClineCapabilityProbe();
|
|
5085
7121
|
try {
|
|
7122
|
+
const effectiveHost = await resolveStartHost(bundle, options);
|
|
7123
|
+
if (effectiveHost === "claude") {
|
|
7124
|
+
await runClaudeStart(bundle, prompt, options);
|
|
7125
|
+
return;
|
|
7126
|
+
}
|
|
7127
|
+
if (effectiveHost === "unsupported") {
|
|
7128
|
+
note(pc.yellow(`Host '${options.host}' has no activation launcher yet; falling back to the Cline lane.`), "Host Runtime");
|
|
7129
|
+
}
|
|
5086
7130
|
const resolution = await launcher.resolveInstallation(bundle, {
|
|
5087
7131
|
global: options.global,
|
|
5088
7132
|
cwd: process.cwd()
|
|
@@ -5093,6 +7137,7 @@ cli.command("start <bundle> [prompt]", "Start an installed bundle team in its ho
|
|
|
5093
7137
|
process.exit(1);
|
|
5094
7138
|
}
|
|
5095
7139
|
const bundleDef = await registry.getBundle(bundle);
|
|
7140
|
+
const organizationTier = bundleDef?.tier === "organization";
|
|
5096
7141
|
const plan = launcher.planActivation({
|
|
5097
7142
|
bundleName: bundle,
|
|
5098
7143
|
workspace: resolution.workspace,
|
|
@@ -5107,6 +7152,8 @@ cli.command("start <bundle> [prompt]", "Start an installed bundle team in its ho
|
|
|
5107
7152
|
if (options.dryRun) {
|
|
5108
7153
|
note(
|
|
5109
7154
|
`Bundle: ${plan.bundleName}
|
|
7155
|
+
Tier: ${bundleDef?.tier ?? "not declared (Tier-1 domain default)"}
|
|
7156
|
+
Agent teams: ${organizationTier ? "not available on this host (claude-only runtime feature)" : "no"}
|
|
5110
7157
|
Scope: ${plan.scope}
|
|
5111
7158
|
Workspace: ${plan.workspace}
|
|
5112
7159
|
Team Name: ${plan.teamName}
|
|
@@ -5118,6 +7165,16 @@ Argv: ${plan.argv.map((a) => a.includes(" ") || a.includes("\n") ? `"${a.replace
|
|
|
5118
7165
|
outro(pc.yellow("Dry run complete. No processes launched."));
|
|
5119
7166
|
return;
|
|
5120
7167
|
}
|
|
7168
|
+
if (organizationTier) {
|
|
7169
|
+
note(
|
|
7170
|
+
pc.yellow(
|
|
7171
|
+
`"${bundle}" is an organization-tier (Tier 2) bundle. Agent Teams is a Claude Code runtime
|
|
7172
|
+
feature and is not available on this host, so '--teams' is claude-only. This session runs
|
|
7173
|
+
Cline's ${plan.strategy} strategy with the projected specialist tools under .cline/agents/.`
|
|
7174
|
+
),
|
|
7175
|
+
"Tier 2 (organization)"
|
|
7176
|
+
);
|
|
7177
|
+
}
|
|
5121
7178
|
note(
|
|
5122
7179
|
`Team: ${pc.bold(plan.teamName)}
|
|
5123
7180
|
Strategy: ${pc.cyan(plan.strategy)}
|
|
@@ -5131,7 +7188,7 @@ Workspace: ${plan.workspace}`,
|
|
|
5131
7188
|
process.exit(1);
|
|
5132
7189
|
}
|
|
5133
7190
|
});
|
|
5134
|
-
cli.command("doctor", "Verify health of installed agents, frontmatter schemas, and hooks").option("--host <host>", "Audit specific host runtime (e.g. cline)").action(async (options = {}) => {
|
|
7191
|
+
cli.command("doctor", "Verify health of installed agents, frontmatter schemas, and hooks").option("--host <host>", "Audit specific host runtime (e.g. cline, claude)").action(async (options = {}) => {
|
|
5135
7192
|
intro(pc.cyan("\u{1FA7A} Agents United \u2014 Health Doctor"));
|
|
5136
7193
|
const report = await DoctorEngine.runDoctor(void 0, options.host);
|
|
5137
7194
|
if (!report.isInitialized || report.agentsCount === 0 && report.skillsCount === 0 && report.workflowsCount === 0 && report.issues.length === 0) {
|
|
@@ -5153,6 +7210,13 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5153
7210
|
console.log(` Named Teams: ${report.clineCapability.namedTeams ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported (Adaptive fallback)")}
|
|
5154
7211
|
`);
|
|
5155
7212
|
}
|
|
7213
|
+
if (report.claudeCapability) {
|
|
7214
|
+
renderClaudeCapabilityBlock(report.claudeCapability);
|
|
7215
|
+
for (const diagnostic of report.claudeCapability.diagnostics) {
|
|
7216
|
+
console.log(` ${pc.dim(diagnostic)}`);
|
|
7217
|
+
}
|
|
7218
|
+
console.log();
|
|
7219
|
+
}
|
|
5156
7220
|
if (report.warnings.length > 0) {
|
|
5157
7221
|
console.log(pc.yellow(pc.bold("\u26A0\uFE0F Warnings:")));
|
|
5158
7222
|
report.warnings.forEach((w) => console.log(` \u26A0 ${w}`));
|
|
@@ -5163,7 +7227,7 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5163
7227
|
}
|
|
5164
7228
|
console.log(` \u{1F916} Installed Agents: ${pc.bold(report.agentsCount.toString())}`);
|
|
5165
7229
|
console.log(` \u26A1 Installed Skills: ${pc.bold(report.skillsCount.toString())}`);
|
|
5166
|
-
console.log(` \u{1F504} Installed
|
|
7230
|
+
console.log(` \u{1F504} Installed Workflow Skills: ${pc.bold(report.workflowsCount.toString())}
|
|
5167
7231
|
`);
|
|
5168
7232
|
if (report.clineCapability) {
|
|
5169
7233
|
console.log(pc.bold(pc.cyan("Cline Runtime & Native Discovery Audit:")));
|
|
@@ -5173,6 +7237,11 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5173
7237
|
}
|
|
5174
7238
|
console.log(` Named Teams: ${report.clineCapability.namedTeams ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported (Adaptive fallback)")}`);
|
|
5175
7239
|
console.log(` Configured Agents: ${report.agentsCount} active natively in any Cline session ("agents start" = optional team-session launcher)
|
|
7240
|
+
`);
|
|
7241
|
+
}
|
|
7242
|
+
if (report.claudeCapability) {
|
|
7243
|
+
renderClaudeCapabilityBlock(report.claudeCapability);
|
|
7244
|
+
console.log(` Configured Agents: ${report.agentsCount} projected into .claude/agents/ ("agents start --host claude" = optional launcher)
|
|
5176
7245
|
`);
|
|
5177
7246
|
}
|
|
5178
7247
|
if (report.issues.length > 0) {
|
|
@@ -5192,15 +7261,176 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5192
7261
|
process.exit(1);
|
|
5193
7262
|
}
|
|
5194
7263
|
});
|
|
7264
|
+
async function resolveStartHost(bundle, options) {
|
|
7265
|
+
const explicitHost = typeof options.host === "string" ? options.host.trim().toLowerCase() : "";
|
|
7266
|
+
if (explicitHost === "claude") {
|
|
7267
|
+
return "claude";
|
|
7268
|
+
}
|
|
7269
|
+
if (explicitHost === "cline") {
|
|
7270
|
+
return "cline";
|
|
7271
|
+
}
|
|
7272
|
+
if (explicitHost.length > 0) {
|
|
7273
|
+
return "unsupported";
|
|
7274
|
+
}
|
|
7275
|
+
let recordedFanout = [];
|
|
7276
|
+
try {
|
|
7277
|
+
const detection = await new ClaudeLauncher().resolveInstallation(bundle, {
|
|
7278
|
+
global: options.global,
|
|
7279
|
+
cwd: process.cwd()
|
|
7280
|
+
});
|
|
7281
|
+
recordedFanout = detection.lockfile.fanout || [];
|
|
7282
|
+
} catch {
|
|
7283
|
+
recordedFanout = [];
|
|
7284
|
+
}
|
|
7285
|
+
return recordedFanout.includes("claude") ? "claude" : "cline";
|
|
7286
|
+
}
|
|
7287
|
+
function resolveClaudeTeamsPosture(teamsOption, tier) {
|
|
7288
|
+
if (teamsOption === true) return { active: true, reason: "explicit", tier };
|
|
7289
|
+
if (teamsOption === false) return { active: false, reason: "opt-out", tier };
|
|
7290
|
+
if (tier === "organization") return { active: true, reason: "tier-default", tier };
|
|
7291
|
+
if (tier === "domain") return { active: false, reason: "domain-tier", tier };
|
|
7292
|
+
return { active: false, reason: "unresolved-tier" };
|
|
7293
|
+
}
|
|
7294
|
+
function describeClaudeTeamsPosture(posture) {
|
|
7295
|
+
switch (posture.reason) {
|
|
7296
|
+
case "explicit":
|
|
7297
|
+
return "explicit --teams";
|
|
7298
|
+
case "tier-default":
|
|
7299
|
+
return "default for tier 'organization'";
|
|
7300
|
+
case "opt-out":
|
|
7301
|
+
return "--no-teams opt-out";
|
|
7302
|
+
case "domain-tier":
|
|
7303
|
+
return "tier 'domain' \u2014 pass --teams to enable";
|
|
7304
|
+
case "unresolved-tier":
|
|
7305
|
+
return "no declared tier (Tier-1 domain default) \u2014 pass --teams to enable";
|
|
7306
|
+
}
|
|
7307
|
+
}
|
|
7308
|
+
async function runClaudeStart(bundle, prompt, options) {
|
|
7309
|
+
const launcher = new ClaudeLauncher();
|
|
7310
|
+
const probe = new ClaudeCapabilityProbe();
|
|
7311
|
+
const resolution = await launcher.resolveInstallation(bundle, {
|
|
7312
|
+
global: options.global,
|
|
7313
|
+
cwd: process.cwd()
|
|
7314
|
+
});
|
|
7315
|
+
const probeReport = await probe.probe();
|
|
7316
|
+
if (!probeReport.installed && !options.dryRun) {
|
|
7317
|
+
outro(pc.red(`Claude Code executable was not found on PATH or via CLAUDE_BIN_PATH. Please install Claude Code or ensure it is accessible.`));
|
|
7318
|
+
process.exit(1);
|
|
7319
|
+
}
|
|
7320
|
+
const bundleDef = await registry.getBundle(bundle);
|
|
7321
|
+
const teamsPosture = resolveClaudeTeamsPosture(options.teams, bundleDef?.tier);
|
|
7322
|
+
const teamsActive = teamsPosture.active;
|
|
7323
|
+
const manifestAvailable = await fs15.pathExists(resolution.manifestPath);
|
|
7324
|
+
const pluginDir = options.plugin ? path17.join(".agents", "plugins", bundle).split(path17.sep).join("/") : void 0;
|
|
7325
|
+
const plan = launcher.planActivation({
|
|
7326
|
+
bundleName: bundle,
|
|
7327
|
+
workspace: resolution.workspace,
|
|
7328
|
+
scope: resolution.scope,
|
|
7329
|
+
report: probeReport,
|
|
7330
|
+
prompt,
|
|
7331
|
+
orchestrator: bundleDef?.orchestrator,
|
|
7332
|
+
allowAddons: options.allowAddons,
|
|
7333
|
+
background: options.bg,
|
|
7334
|
+
teams: teamsActive,
|
|
7335
|
+
pluginDir,
|
|
7336
|
+
manifestAvailable
|
|
7337
|
+
});
|
|
7338
|
+
const recordedFanout = resolution.lockfile.fanout || [];
|
|
7339
|
+
if (options.dryRun) {
|
|
7340
|
+
note(renderClaudeActivationPlan(plan, probeReport, recordedFanout, teamsPosture), "Claude Activation Plan (dry run)");
|
|
7341
|
+
outro(pc.yellow("Dry run complete. No processes launched."));
|
|
7342
|
+
return;
|
|
7343
|
+
}
|
|
7344
|
+
if (!recordedFanout.includes("claude")) {
|
|
7345
|
+
note(
|
|
7346
|
+
pc.yellow(
|
|
7347
|
+
`This workspace records fanout [${recordedFanout.join(", ") || "none"}]. Run
|
|
7348
|
+
'agents update ${bundle} --fanout claude' to project .claude/agents/ before the session needs it.`
|
|
7349
|
+
),
|
|
7350
|
+
"Claude Projection"
|
|
7351
|
+
);
|
|
7352
|
+
}
|
|
7353
|
+
if (teamsActive && teamsPosture.reason === "tier-default") {
|
|
7354
|
+
note(
|
|
7355
|
+
pc.yellow(
|
|
7356
|
+
`Agent Teams is ON by default for this organization-tier bundle (tier 'organization').
|
|
7357
|
+
--no-teams falls back to ordinary parallel subagents.`
|
|
7358
|
+
),
|
|
7359
|
+
"Agent Teams"
|
|
7360
|
+
);
|
|
7361
|
+
} else if (teamsActive) {
|
|
7362
|
+
note(pc.cyan("Agent Teams explicitly requested with --teams; the scaffold stays experimental."), "Agent Teams");
|
|
7363
|
+
}
|
|
7364
|
+
if (teamsActive && !probeReport.agentTeamsExperimental) {
|
|
7365
|
+
note(pc.yellow("The capability probe could not confirm agent-team support from --help; the scaffold stays experimental and unverified."), "Agent Teams");
|
|
7366
|
+
}
|
|
7367
|
+
if (pluginDir && !probeReport.pluginSupport) {
|
|
7368
|
+
note(pc.yellow("The capability probe could not confirm --plugin-dir support from --help."), "Plugin Dir");
|
|
7369
|
+
}
|
|
7370
|
+
note(
|
|
7371
|
+
`Host: ${pc.cyan("claude")}
|
|
7372
|
+
Executable: ${plan.executable}
|
|
7373
|
+
Workspace: ${plan.workspace}
|
|
7374
|
+
Background: ${options.bg ? "yes (--bg)" : "no"}
|
|
7375
|
+
Teams scaffold: ${teamsActive ? `yes (${describeClaudeTeamsPosture(teamsPosture)}; experimental, ephemeral env only)` : `no (${describeClaudeTeamsPosture(teamsPosture)})`}
|
|
7376
|
+
Plugin dir: ${pluginDir || "(none)"}`,
|
|
7377
|
+
"Starting Claude Code Session"
|
|
7378
|
+
);
|
|
7379
|
+
const coordinatorRel = `.claude/agents/${ClaudeLauncher.resolveCoordinatorName(bundleDef?.orchestrator)}.md`;
|
|
7380
|
+
if (!await fs15.pathExists(path17.join(resolution.workspace, coordinatorRel))) {
|
|
7381
|
+
outro(
|
|
7382
|
+
pc.red(
|
|
7383
|
+
`Claude projection "${coordinatorRel}" was not found in ${resolution.workspace}.
|
|
7384
|
+
Run 'agents update ${bundle} --fanout claude' before starting an --agent session.`
|
|
7385
|
+
)
|
|
7386
|
+
);
|
|
7387
|
+
process.exit(1);
|
|
7388
|
+
}
|
|
7389
|
+
await launcher.launch(plan);
|
|
7390
|
+
outro(pc.green(`\u2714 Claude Code session finished.`));
|
|
7391
|
+
}
|
|
7392
|
+
function renderClaudeCapabilityBlock(capability) {
|
|
7393
|
+
console.log(pc.bold(pc.cyan("Claude Code Runtime & Native Discovery Audit:")));
|
|
7394
|
+
console.log(` Installed: ${capability.installed ? pc.green("\u2714 Detected") : pc.yellow("\u2716 Not Found")}`);
|
|
7395
|
+
if (capability.version) {
|
|
7396
|
+
console.log(` Version: ${capability.version}`);
|
|
7397
|
+
}
|
|
7398
|
+
console.log(` Plugin Support (--plugin-dir): ${capability.pluginSupport ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported")}`);
|
|
7399
|
+
console.log(` Agent Teams (experimental): ${capability.agentTeamsExperimental ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported")}`);
|
|
7400
|
+
console.log(` Subagent hand-off (SubagentHandback): ${capability.subagentHandback ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Needs v2.1.271+ (auto mode)")}`);
|
|
7401
|
+
}
|
|
7402
|
+
function renderClaudeActivationPlan(plan, report, recordedFanout, posture) {
|
|
7403
|
+
const envEntries = Object.keys(plan.env);
|
|
7404
|
+
const pluginFlagIndex = plan.argv.indexOf("--plugin-dir");
|
|
7405
|
+
const teamsActive = envEntries.includes("CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS");
|
|
7406
|
+
return [
|
|
7407
|
+
`Bundle: ${plan.bundleName}`,
|
|
7408
|
+
`Host: claude`,
|
|
7409
|
+
`Bundle tier: ${posture.tier ?? "not declared (Tier-1 'domain' default)"}`,
|
|
7410
|
+
`Scope: ${plan.scope}`,
|
|
7411
|
+
`Workspace: ${plan.workspace}`,
|
|
7412
|
+
`Executable: ${plan.executable}`,
|
|
7413
|
+
`Background (--bg): ${plan.argv.includes("--bg") ? "yes" : "no"}`,
|
|
7414
|
+
teamsActive ? `Agent teams (--teams): yes (${describeClaudeTeamsPosture(posture)} \u2014 experimental; ephemeral env only, nothing persisted)` : `Agent teams (--teams): no (${describeClaudeTeamsPosture(posture)})`,
|
|
7415
|
+
`Plugin (--plugin-dir): ${pluginFlagIndex >= 0 ? plan.argv[pluginFlagIndex + 1] : "(none)"}`,
|
|
7416
|
+
`Env injected (merged over process.env): ${envEntries.length > 0 ? envEntries.map((k) => `${k}=${plan.env[k]}`).join(", ") : "(none)"}`,
|
|
7417
|
+
`Capability probe: installed=${report.installed ? `yes${report.version ? ` (${report.version})` : ""}` : "no"}, --plugin-dir=${report.pluginSupport ? "yes" : "no"}, agent teams=${report.agentTeamsExperimental ? "yes" : "no"}, subagent handback=${report.subagentHandback ? "yes" : "no"}`,
|
|
7418
|
+
`Recorded fanout: [${recordedFanout.join(", ")}]`,
|
|
7419
|
+
`Argv (${plan.argv.length} elements, one element per line, spawned with shell: false):`,
|
|
7420
|
+
...plan.argv.map((arg, index) => ` [${index}] ${arg.replace(/\r?\n/g, "\\n")}`)
|
|
7421
|
+
].join("\n");
|
|
7422
|
+
}
|
|
5195
7423
|
cli.help();
|
|
5196
7424
|
cli.version("1.0.0");
|
|
5197
7425
|
cli.parse();
|
|
5198
7426
|
export {
|
|
7427
|
+
describeClaudeTeamsPosture,
|
|
5199
7428
|
detectWorkspaceHosts,
|
|
5200
7429
|
formatInstallationSummary,
|
|
5201
7430
|
formatWrappedList,
|
|
5202
7431
|
getTerminalContentWidth,
|
|
5203
7432
|
renderProjections,
|
|
7433
|
+
resolveClaudeTeamsPosture,
|
|
5204
7434
|
wrapText
|
|
5205
7435
|
};
|
|
5206
7436
|
//# sourceMappingURL=cli.js.map
|