agents-united 0.12.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/dist/cli.d.ts +26 -1
- package/dist/cli.js +2238 -467
- 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 +36 -17
- package/registry/agents/orchestrator-marketing.md +31 -10
- package/registry/agents/orchestrator-research.md +31 -14
- package/registry/agents/orchestrator-security.md +31 -13
- package/registry/agents/orchestrator-system-architecture.md +31 -8
- 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 +18 -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 +20 -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 +290 -32
- 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/translation-ledger.json +26 -0
package/dist/cli.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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";
|
|
@@ -309,6 +309,19 @@ var RegistryResolver = class _RegistryResolver {
|
|
|
309
309
|
`Registry validation error: bundle "${name}" has unknown planningLoop.mode "${mode}". Valid modes: ${validModes.join(", ")}`
|
|
310
310
|
);
|
|
311
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
|
+
}
|
|
312
325
|
if (mode === "planner-orchestrator") {
|
|
313
326
|
if (pl.budget) {
|
|
314
327
|
throw new Error(
|
|
@@ -326,9 +339,9 @@ var RegistryResolver = class _RegistryResolver {
|
|
|
326
339
|
};
|
|
327
340
|
|
|
328
341
|
// src/core/installer.ts
|
|
329
|
-
import
|
|
342
|
+
import path6 from "path";
|
|
330
343
|
import crypto from "crypto";
|
|
331
|
-
import
|
|
344
|
+
import fs4 from "fs-extra";
|
|
332
345
|
|
|
333
346
|
// src/core/adapter.ts
|
|
334
347
|
import path3 from "path";
|
|
@@ -345,7 +358,8 @@ var HOST_REGISTRY = {
|
|
|
345
358
|
agentsSubdir: "agents",
|
|
346
359
|
detectionMarkers: [".agents"],
|
|
347
360
|
profile: "antigravity",
|
|
348
|
-
projectionCapable: false
|
|
361
|
+
projectionCapable: false,
|
|
362
|
+
status: "supported"
|
|
349
363
|
},
|
|
350
364
|
gemini: {
|
|
351
365
|
id: "gemini",
|
|
@@ -355,7 +369,8 @@ var HOST_REGISTRY = {
|
|
|
355
369
|
agentsSubdir: "agents",
|
|
356
370
|
detectionMarkers: [".gemini"],
|
|
357
371
|
profile: "antigravity",
|
|
358
|
-
projectionCapable: false
|
|
372
|
+
projectionCapable: false,
|
|
373
|
+
status: "supported"
|
|
359
374
|
},
|
|
360
375
|
claude: {
|
|
361
376
|
id: "claude",
|
|
@@ -365,7 +380,8 @@ var HOST_REGISTRY = {
|
|
|
365
380
|
agentsSubdir: "agents",
|
|
366
381
|
detectionMarkers: [".claude"],
|
|
367
382
|
profile: "claude-code",
|
|
368
|
-
projectionCapable: true
|
|
383
|
+
projectionCapable: true,
|
|
384
|
+
status: "supported"
|
|
369
385
|
},
|
|
370
386
|
cursor: {
|
|
371
387
|
id: "cursor",
|
|
@@ -375,7 +391,8 @@ var HOST_REGISTRY = {
|
|
|
375
391
|
agentsSubdir: "agents",
|
|
376
392
|
detectionMarkers: [".cursor"],
|
|
377
393
|
profile: "cursor",
|
|
378
|
-
projectionCapable: true
|
|
394
|
+
projectionCapable: true,
|
|
395
|
+
status: "under-development"
|
|
379
396
|
},
|
|
380
397
|
cline: {
|
|
381
398
|
id: "cline",
|
|
@@ -385,7 +402,8 @@ var HOST_REGISTRY = {
|
|
|
385
402
|
agentsSubdir: "agents",
|
|
386
403
|
detectionMarkers: [".cline", ".clinerules"],
|
|
387
404
|
profile: "cline",
|
|
388
|
-
projectionCapable: true
|
|
405
|
+
projectionCapable: true,
|
|
406
|
+
status: "supported"
|
|
389
407
|
},
|
|
390
408
|
opencode: {
|
|
391
409
|
id: "opencode",
|
|
@@ -395,7 +413,8 @@ var HOST_REGISTRY = {
|
|
|
395
413
|
agentsSubdir: "agent",
|
|
396
414
|
detectionMarkers: [".opencode", "opencode.json"],
|
|
397
415
|
profile: "opencode",
|
|
398
|
-
projectionCapable: true
|
|
416
|
+
projectionCapable: true,
|
|
417
|
+
status: "under-development"
|
|
399
418
|
},
|
|
400
419
|
codex: {
|
|
401
420
|
id: "codex",
|
|
@@ -405,13 +424,29 @@ var HOST_REGISTRY = {
|
|
|
405
424
|
agentsSubdir: void 0,
|
|
406
425
|
detectionMarkers: ["AGENTS.md", ".codex"],
|
|
407
426
|
profile: "agentsmd",
|
|
408
|
-
projectionCapable: true
|
|
427
|
+
projectionCapable: true,
|
|
428
|
+
status: "under-development"
|
|
409
429
|
}
|
|
410
430
|
};
|
|
411
431
|
var KNOWN_HOST_IDS = Object.keys(HOST_REGISTRY);
|
|
412
432
|
function isKnownHost(id) {
|
|
413
433
|
return Object.prototype.hasOwnProperty.call(HOST_REGISTRY, id);
|
|
414
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
|
+
}
|
|
415
450
|
function resolveHostProjectDir(host, cwd) {
|
|
416
451
|
return path2.resolve(cwd, HOST_REGISTRY[host].projectDir);
|
|
417
452
|
}
|
|
@@ -682,7 +717,8 @@ ${bodyStr}
|
|
|
682
717
|
const coordinatorCanonical = `agents/${coordinatorFile}`;
|
|
683
718
|
const specialistLines = (bundle.agents || []).map((agentFile) => {
|
|
684
719
|
const name = agentFile.replace(/\.md$/, "");
|
|
685
|
-
|
|
720
|
+
const projected = this.stripSubagentPrefix(name);
|
|
721
|
+
return `- **${name}**: \`.cline/agents/${projected}.yml\` | \`.agents/agents/${agentFile}\``;
|
|
686
722
|
});
|
|
687
723
|
const rawWorkflows = [
|
|
688
724
|
...(bundle.workflows || []).map((w) => w.replace(/\.md$/i, "")),
|
|
@@ -1065,21 +1101,779 @@ ${normalizedBody}`;
|
|
|
1065
1101
|
}
|
|
1066
1102
|
};
|
|
1067
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
|
+
|
|
1068
1847
|
// src/core/installer.ts
|
|
1069
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
|
+
|
|
1863
|
+
// src/core/installer.ts
|
|
1070
1864
|
var InstallEngine = class _InstallEngine {
|
|
1071
1865
|
registry;
|
|
1072
1866
|
constructor(registry2) {
|
|
1073
1867
|
this.registry = registry2 || new RegistryResolver();
|
|
1074
1868
|
}
|
|
1075
1869
|
async calculateHash(filePath) {
|
|
1076
|
-
const buffer = await
|
|
1870
|
+
const buffer = await fs4.readFile(filePath);
|
|
1077
1871
|
return "sha256:" + crypto.createHash("sha256").update(buffer).digest("hex");
|
|
1078
1872
|
}
|
|
1079
1873
|
async readLockfile(lockfilePath) {
|
|
1080
|
-
if (await
|
|
1874
|
+
if (await fs4.pathExists(lockfilePath)) {
|
|
1081
1875
|
try {
|
|
1082
|
-
return await
|
|
1876
|
+
return await fs4.readJson(lockfilePath);
|
|
1083
1877
|
} catch {
|
|
1084
1878
|
}
|
|
1085
1879
|
}
|
|
@@ -1122,24 +1916,24 @@ var InstallEngine = class _InstallEngine {
|
|
|
1122
1916
|
return options.scope || "project";
|
|
1123
1917
|
}
|
|
1124
1918
|
async deployFile(src, dest, method, force) {
|
|
1125
|
-
await
|
|
1126
|
-
if (await
|
|
1919
|
+
await fs4.ensureDir(path6.dirname(dest));
|
|
1920
|
+
if (await fs4.pathExists(dest) || await fs4.pathExists(dest).catch(() => false)) {
|
|
1127
1921
|
if (force) {
|
|
1128
|
-
await
|
|
1922
|
+
await fs4.remove(dest);
|
|
1129
1923
|
}
|
|
1130
1924
|
}
|
|
1131
1925
|
if (method === "symlink") {
|
|
1132
1926
|
try {
|
|
1133
|
-
const stat = await
|
|
1927
|
+
const stat = await fs4.stat(src);
|
|
1134
1928
|
const type = stat.isDirectory() ? process.platform === "win32" ? "junction" : "dir" : "file";
|
|
1135
|
-
await
|
|
1929
|
+
await fs4.ensureSymlink(src, dest, type);
|
|
1136
1930
|
return "symlink";
|
|
1137
1931
|
} catch {
|
|
1138
|
-
await
|
|
1932
|
+
await fs4.copy(src, dest, { overwrite: true });
|
|
1139
1933
|
return "copy";
|
|
1140
1934
|
}
|
|
1141
1935
|
}
|
|
1142
|
-
await
|
|
1936
|
+
await fs4.copy(src, dest, { overwrite: true });
|
|
1143
1937
|
return "copy";
|
|
1144
1938
|
}
|
|
1145
1939
|
/**
|
|
@@ -1147,8 +1941,8 @@ var InstallEngine = class _InstallEngine {
|
|
|
1147
1941
|
* copies of canonical assets, so their bytes differ from the source.
|
|
1148
1942
|
*/
|
|
1149
1943
|
async deployProjection(dest, content) {
|
|
1150
|
-
await
|
|
1151
|
-
await
|
|
1944
|
+
await fs4.ensureDir(path6.dirname(dest));
|
|
1945
|
+
await fs4.writeFile(dest, content, "utf8");
|
|
1152
1946
|
}
|
|
1153
1947
|
/**
|
|
1154
1948
|
* Computes the canonical `.agents/` target dir and the workspace root that contains
|
|
@@ -1156,27 +1950,27 @@ var InstallEngine = class _InstallEngine {
|
|
|
1156
1950
|
*/
|
|
1157
1951
|
static projectionRoot(scope, targetDir) {
|
|
1158
1952
|
const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", targetDir);
|
|
1159
|
-
return { agentsTarget, root:
|
|
1953
|
+
return { agentsTarget, root: path6.resolve(path6.dirname(agentsTarget)) };
|
|
1160
1954
|
}
|
|
1161
1955
|
toPosix(p) {
|
|
1162
|
-
return p.split(
|
|
1956
|
+
return p.split(path6.sep).join("/");
|
|
1163
1957
|
}
|
|
1164
1958
|
canonicalRelAgent(agentFile) {
|
|
1165
1959
|
return `.agents/agents/${agentFile}`;
|
|
1166
1960
|
}
|
|
1167
1961
|
lockRel(subdir, file) {
|
|
1168
|
-
return
|
|
1962
|
+
return path6.join(subdir, file);
|
|
1169
1963
|
}
|
|
1170
1964
|
destForProjection(host, root, file) {
|
|
1171
1965
|
const def = HOST_REGISTRY[host];
|
|
1172
1966
|
const base = resolveHostProjectDir(host, root);
|
|
1173
|
-
if (def.agentsSubdir) return
|
|
1967
|
+
if (def.agentsSubdir) return path6.join(base, def.agentsSubdir, file);
|
|
1174
1968
|
return base;
|
|
1175
1969
|
}
|
|
1176
1970
|
recordProjectedTo(lockfile, relKey, projPath) {
|
|
1177
1971
|
const normProj = this.toPosix(projPath);
|
|
1178
1972
|
const posixKey = relKey.replace(/\\/g, "/");
|
|
1179
|
-
const sysKey = relKey.replace(/\//g,
|
|
1973
|
+
const sysKey = relKey.replace(/\//g, path6.sep);
|
|
1180
1974
|
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : relKey;
|
|
1181
1975
|
if (!lockfile.files[targetKey]) {
|
|
1182
1976
|
lockfile.files[targetKey] = { hash: "", installedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -1190,7 +1984,7 @@ var InstallEngine = class _InstallEngine {
|
|
|
1190
1984
|
removeProjectedTo(lockfile, canonicalRelPath, projectedRelPath) {
|
|
1191
1985
|
const normProj = this.toPosix(projectedRelPath);
|
|
1192
1986
|
const posixKey = canonicalRelPath.replace(/\\/g, "/");
|
|
1193
|
-
const sysKey = canonicalRelPath.replace(/\//g,
|
|
1987
|
+
const sysKey = canonicalRelPath.replace(/\//g, path6.sep);
|
|
1194
1988
|
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : canonicalRelPath;
|
|
1195
1989
|
const asset = lockfile.files[targetKey];
|
|
1196
1990
|
if (asset && asset.projectedTo) {
|
|
@@ -1247,7 +2041,7 @@ var InstallEngine = class _InstallEngine {
|
|
|
1247
2041
|
if (prefixes.length === 0 || producedByCanonical.size === 0) return;
|
|
1248
2042
|
for (const [canonical, producedPaths] of producedByCanonical) {
|
|
1249
2043
|
const posixKey = canonical.replace(/\\/g, "/");
|
|
1250
|
-
const sysKey = canonical.replace(/\//g,
|
|
2044
|
+
const sysKey = canonical.replace(/\//g, path6.sep);
|
|
1251
2045
|
const targetKey = lockfile.files[posixKey] ? posixKey : lockfile.files[sysKey] ? sysKey : null;
|
|
1252
2046
|
if (!targetKey) continue;
|
|
1253
2047
|
const asset = lockfile.files[targetKey];
|
|
@@ -1264,17 +2058,142 @@ var InstallEngine = class _InstallEngine {
|
|
|
1264
2058
|
}
|
|
1265
2059
|
}
|
|
1266
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
|
+
}
|
|
1267
2186
|
async removeEmptyProjectionDirs(workspaceRoot, projPath) {
|
|
1268
|
-
let dir =
|
|
2187
|
+
let dir = path6.dirname(path6.join(workspaceRoot, projPath));
|
|
1269
2188
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
1270
2189
|
try {
|
|
1271
|
-
const entries = await
|
|
2190
|
+
const entries = await fs4.readdir(dir);
|
|
1272
2191
|
if (entries.length > 0) break;
|
|
1273
|
-
await
|
|
2192
|
+
await fs4.rmdir(dir);
|
|
1274
2193
|
} catch {
|
|
1275
2194
|
break;
|
|
1276
2195
|
}
|
|
1277
|
-
dir =
|
|
2196
|
+
dir = path6.dirname(dir);
|
|
1278
2197
|
}
|
|
1279
2198
|
}
|
|
1280
2199
|
// ADR 0016: Silent auto-migration of legacy .agents/workflows/*.md into
|
|
@@ -1284,27 +2203,27 @@ var InstallEngine = class _InstallEngine {
|
|
|
1284
2203
|
let migratedCount = 0;
|
|
1285
2204
|
let targetLockfile = lockfile;
|
|
1286
2205
|
let lockfileLoadedFromDisk = false;
|
|
1287
|
-
if (!targetLockfile && await
|
|
2206
|
+
if (!targetLockfile && await fs4.pathExists(subPaths.lockfile)) {
|
|
1288
2207
|
try {
|
|
1289
|
-
targetLockfile = await
|
|
2208
|
+
targetLockfile = await fs4.readJson(subPaths.lockfile);
|
|
1290
2209
|
lockfileLoadedFromDisk = true;
|
|
1291
2210
|
} catch {
|
|
1292
2211
|
}
|
|
1293
2212
|
}
|
|
1294
|
-
if (await
|
|
1295
|
-
const entries = await
|
|
2213
|
+
if (await fs4.pathExists(subPaths.workflowsDir)) {
|
|
2214
|
+
const entries = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
|
|
1296
2215
|
for (const entry of entries) {
|
|
1297
2216
|
if (entry.endsWith(".md")) {
|
|
1298
|
-
const legacyPath =
|
|
2217
|
+
const legacyPath = path6.join(subPaths.workflowsDir, entry);
|
|
1299
2218
|
let skillName = entry.replace(/\.md$/, "").replace(/--/g, "-");
|
|
1300
2219
|
if (!skillName.startsWith("workflow-")) {
|
|
1301
2220
|
skillName = `workflow-${skillName}`;
|
|
1302
2221
|
}
|
|
1303
|
-
const targetSkillDir =
|
|
1304
|
-
const targetSkillFile =
|
|
1305
|
-
if (!await
|
|
1306
|
-
await
|
|
1307
|
-
const legacyContent = await
|
|
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");
|
|
1308
2227
|
let skillContent = legacyContent;
|
|
1309
2228
|
const fmMatch = legacyContent.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
1310
2229
|
if (fmMatch) {
|
|
@@ -1325,14 +2244,14 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1325
2244
|
} catch {
|
|
1326
2245
|
}
|
|
1327
2246
|
}
|
|
1328
|
-
await
|
|
2247
|
+
await fs4.writeFile(targetSkillFile, skillContent, "utf8");
|
|
1329
2248
|
}
|
|
1330
|
-
await
|
|
2249
|
+
await fs4.remove(legacyPath).catch(() => {
|
|
1331
2250
|
});
|
|
1332
2251
|
migratedCount++;
|
|
1333
2252
|
if (targetLockfile) {
|
|
1334
|
-
const legacyRel = this.toPosix(
|
|
1335
|
-
const newRel = this.toPosix(
|
|
2253
|
+
const legacyRel = this.toPosix(path6.relative(targetDir, legacyPath));
|
|
2254
|
+
const newRel = this.toPosix(path6.relative(targetDir, targetSkillFile));
|
|
1336
2255
|
if (targetLockfile.files && targetLockfile.files[legacyRel]) {
|
|
1337
2256
|
targetLockfile.files[newRel] = targetLockfile.files[legacyRel];
|
|
1338
2257
|
delete targetLockfile.files[legacyRel];
|
|
@@ -1345,9 +2264,9 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1345
2264
|
}
|
|
1346
2265
|
}
|
|
1347
2266
|
}
|
|
1348
|
-
const remaining = await
|
|
2267
|
+
const remaining = await fs4.readdir(subPaths.workflowsDir).catch(() => []);
|
|
1349
2268
|
if (remaining.length === 0) {
|
|
1350
|
-
await
|
|
2269
|
+
await fs4.remove(subPaths.workflowsDir).catch(() => {
|
|
1351
2270
|
});
|
|
1352
2271
|
}
|
|
1353
2272
|
}
|
|
@@ -1363,15 +2282,15 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1363
2282
|
}
|
|
1364
2283
|
targetLockfile.installed.workflows = [];
|
|
1365
2284
|
}
|
|
1366
|
-
if (targetLockfile && (lockfileLoadedFromDisk || await
|
|
1367
|
-
await
|
|
2285
|
+
if (targetLockfile && (lockfileLoadedFromDisk || await fs4.pathExists(subPaths.lockfile))) {
|
|
2286
|
+
await fs4.writeJson(subPaths.lockfile, targetLockfile, { spaces: 2 });
|
|
1368
2287
|
}
|
|
1369
2288
|
return migratedCount;
|
|
1370
2289
|
}
|
|
1371
2290
|
/**
|
|
1372
2291
|
* Dry-run: compute the projection plan (paths + warnings) without writing anything.
|
|
1373
2292
|
*/
|
|
1374
|
-
async buildProjections(fanoutHosts, resolved, registryDir, scope, targetDir) {
|
|
2293
|
+
async buildProjections(fanoutHosts, resolved, registryDir, scope, targetDir, pluginLane = false) {
|
|
1375
2294
|
const infos = [];
|
|
1376
2295
|
if (fanoutHosts.length === 0) return infos;
|
|
1377
2296
|
const { root } = _InstallEngine.projectionRoot(scope, targetDir);
|
|
@@ -1380,10 +2299,17 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1380
2299
|
infos.push({ host, path: "AGENTS.md", kind: "bridge", warnings: [] });
|
|
1381
2300
|
continue;
|
|
1382
2301
|
}
|
|
1383
|
-
if (host === "cline") {
|
|
2302
|
+
if (host === "cline" || host === "claude") {
|
|
1384
2303
|
const bundleDef = resolved.targetBundle ? await this.registry.getBundle(resolved.targetBundle) : void 0;
|
|
1385
2304
|
if (bundleDef) {
|
|
1386
|
-
const artifacts = await
|
|
2305
|
+
const artifacts = await _InstallEngine.planCompoundArtifacts(
|
|
2306
|
+
host,
|
|
2307
|
+
bundleDef,
|
|
2308
|
+
scope,
|
|
2309
|
+
resolved,
|
|
2310
|
+
registryDir,
|
|
2311
|
+
pluginLane
|
|
2312
|
+
);
|
|
1387
2313
|
for (const artifact of artifacts) {
|
|
1388
2314
|
infos.push({ host, path: artifact.relPath, kind: artifact.kind, warnings: [] });
|
|
1389
2315
|
}
|
|
@@ -1393,10 +2319,10 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1393
2319
|
const base = resolveHostProjectDir(host, root);
|
|
1394
2320
|
const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
|
|
1395
2321
|
for (const agentFile of resolved.agents) {
|
|
1396
|
-
const content = await
|
|
2322
|
+
const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
|
|
1397
2323
|
const res = HostProjector.projectAgent(content, HOST_REGISTRY[host].profile, this.canonicalRelAgent(agentFile));
|
|
1398
|
-
const dest =
|
|
1399
|
-
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 });
|
|
1400
2326
|
}
|
|
1401
2327
|
}
|
|
1402
2328
|
return infos;
|
|
@@ -1410,6 +2336,14 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1410
2336
|
if (fanoutHosts.length === 0) return;
|
|
1411
2337
|
const { root } = _InstallEngine.projectionRoot(scope, options.targetDir);
|
|
1412
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
|
+
}
|
|
1413
2347
|
for (const host of fanoutHosts) {
|
|
1414
2348
|
if (host === "codex") {
|
|
1415
2349
|
const assets = [];
|
|
@@ -1417,9 +2351,9 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1417
2351
|
resolved.workflows.forEach((f) => assets.push({ name: f.replace(/\.md$/, ""), type: "workflow", relPath: `.agents/workflows/${f}` }));
|
|
1418
2352
|
resolved.skills.forEach((s) => assets.push({ name: s, type: "skill", relPath: `.agents/skills/${s}/SKILL.md` }));
|
|
1419
2353
|
const bridge = HostProjector.buildAgentsMdIndex(assets);
|
|
1420
|
-
const bridgeDest =
|
|
1421
|
-
if (await
|
|
1422
|
-
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");
|
|
1423
2357
|
if (!HostProjector.hasManagedMarker(existing)) {
|
|
1424
2358
|
throw new Error(
|
|
1425
2359
|
"File AGENTS.md already exists at the workspace root and is not managed by agents-united. Use --force to overwrite."
|
|
@@ -1430,10 +2364,10 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1430
2364
|
projections.push({ host, path: "AGENTS.md", kind: "bridge", warnings: [] });
|
|
1431
2365
|
resolved.agents.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("agents", f), "AGENTS.md"));
|
|
1432
2366
|
resolved.workflows.forEach((f) => this.recordProjectedTo(lockfile, this.lockRel("workflows", f), "AGENTS.md"));
|
|
1433
|
-
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"));
|
|
1434
2368
|
continue;
|
|
1435
2369
|
}
|
|
1436
|
-
if (host === "cline") {
|
|
2370
|
+
if (host === "cline" || host === "claude") {
|
|
1437
2371
|
const bundleDef = resolved.targetBundle ? await this.registry.getBundle(resolved.targetBundle) : void 0;
|
|
1438
2372
|
if (bundleDef) {
|
|
1439
2373
|
const declared = /* @__PURE__ */ new Set();
|
|
@@ -1441,84 +2375,23 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1441
2375
|
(bundleDef.agents || []).forEach((a) => declared.add(`agents/${a}`));
|
|
1442
2376
|
(bundleDef.skills || []).forEach((s) => declared.add(`skills/${s}/SKILL.md`));
|
|
1443
2377
|
(bundleDef.workflows || []).forEach((w) => declared.add(`workflows/${w}`));
|
|
1444
|
-
const artifacts = await
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
for (const artifact of artifacts) {
|
|
1465
|
-
const dest = path5.join(root, artifact.relPath);
|
|
1466
|
-
if (await fs3.pathExists(dest) && !options.force) {
|
|
1467
|
-
const existing = await fs3.readFile(dest, "utf8").catch(() => null);
|
|
1468
|
-
if (existing !== null) {
|
|
1469
|
-
if (artifact.managedMarker && !HostProjector.hasManagedMarker(existing)) {
|
|
1470
|
-
throw new Error(
|
|
1471
|
-
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
1472
|
-
);
|
|
1473
|
-
} else if (!artifact.managedMarker) {
|
|
1474
|
-
const registeredProj = lockfile.projections?.[artifact.relPath];
|
|
1475
|
-
if (!registeredProj) {
|
|
1476
|
-
throw new Error(
|
|
1477
|
-
`Projection target ${artifact.relPath} already exists and is not managed by agents-united. Use --force to overwrite.`
|
|
1478
|
-
);
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (artifact.content !== void 0) {
|
|
1484
|
-
await this.deployProjection(dest, artifact.content);
|
|
1485
|
-
} else if (artifact.sourceFilePath) {
|
|
1486
|
-
await fs3.ensureDir(path5.dirname(dest));
|
|
1487
|
-
await fs3.copy(artifact.sourceFilePath, dest, { overwrite: true });
|
|
1488
|
-
}
|
|
1489
|
-
const deployedHash = await this.calculateHash(dest);
|
|
1490
|
-
lockfile.projections = lockfile.projections || {};
|
|
1491
|
-
const existingProj = lockfile.projections[artifact.relPath];
|
|
1492
|
-
const bundleName = bundleDef.name;
|
|
1493
|
-
const declares = artifact.kind === "rule" || artifact.kind === "team-manifest" || artifact.kind === "plugin-manifest" || (artifact.canonical ? declared.has(artifact.canonical) : false);
|
|
1494
|
-
const priorOwners = existingProj?.owners ?? [];
|
|
1495
|
-
const owners = declares ? Array.from(/* @__PURE__ */ new Set([...priorOwners, bundleName])) : priorOwners;
|
|
1496
|
-
lockfile.projections[artifact.relPath] = {
|
|
1497
|
-
host: "cline",
|
|
1498
|
-
kind: artifact.kind,
|
|
1499
|
-
canonical: artifact.canonical,
|
|
1500
|
-
owners,
|
|
1501
|
-
hash: deployedHash,
|
|
1502
|
-
installedAt: existingProj?.installedAt ?? now,
|
|
1503
|
-
managedMarker: artifact.managedMarker
|
|
1504
|
-
};
|
|
1505
|
-
projections.push({ host, path: artifact.relPath, kind: artifact.kind, warnings: [] });
|
|
1506
|
-
if (artifact.canonical) {
|
|
1507
|
-
this.recordProjectedTo(lockfile, artifact.canonical, artifact.relPath);
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
const producedByCanonical = /* @__PURE__ */ new Map();
|
|
1511
|
-
for (const artifact of artifacts) {
|
|
1512
|
-
if (!artifact.canonical) continue;
|
|
1513
|
-
const producedPaths = producedByCanonical.get(artifact.canonical);
|
|
1514
|
-
if (producedPaths) {
|
|
1515
|
-
producedPaths.push(artifact.relPath);
|
|
1516
|
-
} else {
|
|
1517
|
-
producedByCanonical.set(artifact.canonical, [artifact.relPath]);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
this.reconcileProjectedTo(lockfile, "cline", bundleDef.name, producedByCanonical);
|
|
1521
|
-
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) {
|
|
1522
2395
|
const parentDef = await this.registry.getBundle(bundleDef.parentBundle);
|
|
1523
2396
|
if (parentDef) {
|
|
1524
2397
|
const parentProjection = await ClineProjector.planCompoundProjection(
|
|
@@ -1531,8 +2404,8 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1531
2404
|
for (const artifact of parentProjection) {
|
|
1532
2405
|
if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
|
|
1533
2406
|
if (artifact.content === void 0) continue;
|
|
1534
|
-
const dest =
|
|
1535
|
-
if (!await
|
|
2407
|
+
const dest = path6.join(root, artifact.relPath);
|
|
2408
|
+
if (!await fs4.pathExists(dest)) continue;
|
|
1536
2409
|
await this.deployProjection(dest, artifact.content);
|
|
1537
2410
|
const deployedHash = await this.calculateHash(dest);
|
|
1538
2411
|
lockfile.projections = lockfile.projections || {};
|
|
@@ -1556,35 +2429,51 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1556
2429
|
const base = resolveHostProjectDir(host, root);
|
|
1557
2430
|
const subdir = HOST_REGISTRY[host].agentsSubdir ?? "agents";
|
|
1558
2431
|
for (const agentFile of resolved.agents) {
|
|
1559
|
-
const content = await
|
|
2432
|
+
const content = await fs4.readFile(path6.join(registryDir, "agents", agentFile), "utf8");
|
|
1560
2433
|
const canonicalRel = this.canonicalRelAgent(agentFile);
|
|
1561
|
-
const
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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
|
+
}
|
|
1567
2449
|
}
|
|
1568
|
-
if (await
|
|
1569
|
-
const existing = await
|
|
2450
|
+
if (await fs4.pathExists(dest) && !options.force) {
|
|
2451
|
+
const existing = await fs4.readFile(dest, "utf8");
|
|
1570
2452
|
if (!HostProjector.hasManagedMarker(existing)) {
|
|
1571
2453
|
throw new Error(
|
|
1572
|
-
`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.`
|
|
1573
2455
|
);
|
|
1574
2456
|
}
|
|
1575
2457
|
}
|
|
1576
2458
|
await this.deployProjection(dest, res.content);
|
|
1577
|
-
projections.push({
|
|
2459
|
+
projections.push({
|
|
2460
|
+
host,
|
|
2461
|
+
path: projPath,
|
|
2462
|
+
kind: "role",
|
|
2463
|
+
warnings: "warnings" in res ? res.warnings : []
|
|
2464
|
+
});
|
|
1578
2465
|
this.recordProjectedTo(lockfile, this.lockRel("agents", agentFile), projPath);
|
|
1579
|
-
if (host === "cline") {
|
|
2466
|
+
if (host === "cline" || host === "claude") {
|
|
1580
2467
|
lockfile.projections = lockfile.projections || {};
|
|
2468
|
+
const existingProj = lockfile.projections[projPath];
|
|
2469
|
+
const canonicalRecord = lockfile.files[canonicalRel] ?? lockfile.files[canonicalRel.replace(/^\.agents\//, "")];
|
|
1581
2470
|
lockfile.projections[projPath] = {
|
|
1582
2471
|
host,
|
|
1583
2472
|
kind: "role",
|
|
1584
2473
|
canonical: canonicalRel,
|
|
1585
|
-
owners: resolved.targetBundle
|
|
2474
|
+
owners: mergeAssetOwners(existingProj, resolved.targetBundle ?? void 0).concat(canonicalRecord?.owners ?? []).filter((o, i, arr) => arr.indexOf(o) === i),
|
|
1586
2475
|
hash: await this.calculateHash(dest),
|
|
1587
|
-
installedAt:
|
|
2476
|
+
installedAt: existingProj?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1588
2477
|
managedMarker: true
|
|
1589
2478
|
};
|
|
1590
2479
|
}
|
|
@@ -1602,17 +2491,19 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1602
2491
|
const fanoutHosts = (options.fanout || []).map((h) => typeof h === "string" ? h.trim().toLowerCase() : h).filter((h) => isKnownHost(h) && HOST_REGISTRY[h]?.projectionCapable);
|
|
1603
2492
|
if (options.dryRun) {
|
|
1604
2493
|
let effectiveDryFanout = fanoutHosts;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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;
|
|
1613
2504
|
}
|
|
1614
2505
|
}
|
|
1615
|
-
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) : [];
|
|
1616
2507
|
return { installed: resolved, targetDirs, dryRun: true, method, projections: projections2 };
|
|
1617
2508
|
}
|
|
1618
2509
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1620,12 +2511,12 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1620
2511
|
const agentsTarget = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
|
|
1621
2512
|
for (const targetDir of targetDirs) {
|
|
1622
2513
|
const subPaths = AgentHostAdapter.getSubPaths(targetDir);
|
|
1623
|
-
await
|
|
1624
|
-
await
|
|
2514
|
+
await fs4.ensureDir(subPaths.agentsDir);
|
|
2515
|
+
await fs4.ensureDir(subPaths.skillsDir);
|
|
1625
2516
|
if (resolved.workflows && resolved.workflows.length > 0) {
|
|
1626
|
-
await
|
|
2517
|
+
await fs4.ensureDir(subPaths.workflowsDir);
|
|
1627
2518
|
}
|
|
1628
|
-
await
|
|
2519
|
+
await fs4.ensureDir(subPaths.rulesDir);
|
|
1629
2520
|
const lockfile = await this.readLockfile(subPaths.lockfile);
|
|
1630
2521
|
await this.migrateLegacyWorkspaceWorkflows(targetDir, lockfile);
|
|
1631
2522
|
lockfile.scope = scope;
|
|
@@ -1636,32 +2527,34 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1636
2527
|
lockfile.fanout = effectiveFanout;
|
|
1637
2528
|
}
|
|
1638
2529
|
const declared = /* @__PURE__ */ new Set();
|
|
2530
|
+
let hasDeclaredRoster = false;
|
|
1639
2531
|
if (resolved.targetBundle) {
|
|
1640
2532
|
const bundleDef = await this.registry.getBundle(resolved.targetBundle);
|
|
1641
2533
|
if (bundleDef) {
|
|
2534
|
+
hasDeclaredRoster = true;
|
|
1642
2535
|
if (bundleDef.orchestrator) declared.add(`agents/${bundleDef.orchestrator}`);
|
|
1643
2536
|
(bundleDef.agents || []).forEach((a) => declared.add(`agents/${a}`));
|
|
1644
2537
|
(bundleDef.skills || []).forEach((s) => declared.add(`skills/${s}/SKILL.md`));
|
|
1645
2538
|
(bundleDef.workflows || []).forEach((w) => declared.add(`workflows/${w}`));
|
|
1646
2539
|
}
|
|
1647
2540
|
}
|
|
2541
|
+
const declaresAsset = (assetKey) => !hasDeclaredRoster || declared.has(assetKey);
|
|
1648
2542
|
for (const agentFile of resolved.agents) {
|
|
1649
|
-
const src =
|
|
1650
|
-
const dest =
|
|
1651
|
-
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") {
|
|
1652
2546
|
const existingHash = await this.calculateHash(dest).catch(() => null);
|
|
1653
|
-
const relPath2 =
|
|
2547
|
+
const relPath2 = path6.relative(targetDir, dest);
|
|
1654
2548
|
if (existingHash && lockfile.files[relPath2] && lockfile.files[relPath2].hash !== existingHash) {
|
|
1655
2549
|
throw new Error(`File ${relPath2} has user modifications. Use --force to overwrite.`);
|
|
1656
2550
|
}
|
|
1657
2551
|
}
|
|
1658
2552
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1659
2553
|
const hash = await this.calculateHash(src);
|
|
1660
|
-
const relPath = this.toPosix(
|
|
2554
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1661
2555
|
const existing = lockfile.files[relPath];
|
|
1662
|
-
const existingOwners = existing?.owners ??
|
|
1663
|
-
const
|
|
1664
|
-
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;
|
|
1665
2558
|
lockfile.files[relPath] = {
|
|
1666
2559
|
hash,
|
|
1667
2560
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1675,17 +2568,16 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1675
2568
|
}
|
|
1676
2569
|
}
|
|
1677
2570
|
for (const skillName of resolved.skills) {
|
|
1678
|
-
const src =
|
|
1679
|
-
const dest =
|
|
2571
|
+
const src = path6.join(registryDir, "skills", skillName);
|
|
2572
|
+
const dest = path6.join(subPaths.skillsDir, skillName);
|
|
1680
2573
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1681
|
-
const skillFile =
|
|
1682
|
-
if (await
|
|
2574
|
+
const skillFile = path6.join(src, "SKILL.md");
|
|
2575
|
+
if (await fs4.pathExists(skillFile)) {
|
|
1683
2576
|
const hash = await this.calculateHash(skillFile);
|
|
1684
|
-
const relPath = this.toPosix(
|
|
2577
|
+
const relPath = this.toPosix(path6.relative(targetDir, path6.join(subPaths.skillsDir, skillName, "SKILL.md")));
|
|
1685
2578
|
const existing = lockfile.files[relPath];
|
|
1686
|
-
const existingOwners = existing?.owners ??
|
|
1687
|
-
const
|
|
1688
|
-
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;
|
|
1689
2581
|
lockfile.files[relPath] = {
|
|
1690
2582
|
hash,
|
|
1691
2583
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1695,20 +2587,43 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1695
2587
|
...existing?.projectedTo ? { projectedTo: existing.projectedTo } : {}
|
|
1696
2588
|
};
|
|
1697
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
|
+
}
|
|
1698
2614
|
if (!lockfile.installed.skills.includes(skillName)) {
|
|
1699
2615
|
lockfile.installed.skills.push(skillName);
|
|
1700
2616
|
}
|
|
1701
2617
|
}
|
|
1702
2618
|
for (const workflowFile of resolved.workflows) {
|
|
1703
|
-
const src =
|
|
1704
|
-
const dest =
|
|
2619
|
+
const src = path6.join(registryDir, "workflows", workflowFile);
|
|
2620
|
+
const dest = path6.join(subPaths.workflowsDir, workflowFile);
|
|
1705
2621
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1706
2622
|
const hash = await this.calculateHash(src);
|
|
1707
|
-
const relPath = this.toPosix(
|
|
2623
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1708
2624
|
const existing = lockfile.files[relPath];
|
|
1709
|
-
const existingOwners = existing?.owners ??
|
|
1710
|
-
const
|
|
1711
|
-
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;
|
|
1712
2627
|
lockfile.files[relPath] = {
|
|
1713
2628
|
hash,
|
|
1714
2629
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1723,16 +2638,15 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1723
2638
|
}
|
|
1724
2639
|
}
|
|
1725
2640
|
for (const ruleFile of resolved.rules) {
|
|
1726
|
-
const src =
|
|
1727
|
-
const dest =
|
|
1728
|
-
if (await
|
|
2641
|
+
const src = path6.join(registryDir, "rules", ruleFile);
|
|
2642
|
+
const dest = path6.join(subPaths.rulesDir, ruleFile);
|
|
2643
|
+
if (await fs4.pathExists(src)) {
|
|
1729
2644
|
const actualMethod = await this.deployFile(src, dest, method, options.force);
|
|
1730
2645
|
const hash = await this.calculateHash(src);
|
|
1731
|
-
const relPath = this.toPosix(
|
|
2646
|
+
const relPath = this.toPosix(path6.relative(targetDir, dest));
|
|
1732
2647
|
const existing = lockfile.files[relPath];
|
|
1733
|
-
const existingOwners = existing?.owners ??
|
|
1734
|
-
const
|
|
1735
|
-
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;
|
|
1736
2650
|
lockfile.files[relPath] = {
|
|
1737
2651
|
hash,
|
|
1738
2652
|
bundle: existing?.bundle ?? resolved.targetBundle,
|
|
@@ -1755,26 +2669,26 @@ ${YAML2.stringify(meta).trim()}
|
|
|
1755
2669
|
lockfile.bundleModes[resolved.targetBundle] = options.mode;
|
|
1756
2670
|
}
|
|
1757
2671
|
}
|
|
1758
|
-
if (hasCanonicalAgents && effectiveFanout.length > 0 &&
|
|
2672
|
+
if (hasCanonicalAgents && effectiveFanout.length > 0 && path6.resolve(targetDir) === path6.resolve(agentsTarget)) {
|
|
1759
2673
|
await this.applyFanout(effectiveFanout, resolved, registryDir, scope, targetDir, lockfile, options, projections);
|
|
1760
2674
|
}
|
|
1761
|
-
await
|
|
2675
|
+
await fs4.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
1762
2676
|
}
|
|
1763
2677
|
return { installed: resolved, targetDirs, dryRun: false, method, projections };
|
|
1764
2678
|
}
|
|
1765
2679
|
};
|
|
1766
2680
|
|
|
1767
2681
|
// src/core/uninstaller.ts
|
|
1768
|
-
import
|
|
2682
|
+
import path7 from "path";
|
|
1769
2683
|
import crypto2 from "crypto";
|
|
1770
|
-
import
|
|
2684
|
+
import fs5 from "fs-extra";
|
|
1771
2685
|
var UninstallEngine = class {
|
|
1772
2686
|
registry;
|
|
1773
2687
|
constructor(registry2) {
|
|
1774
2688
|
this.registry = registry2 || new RegistryResolver();
|
|
1775
2689
|
}
|
|
1776
2690
|
async calculateHash(filePath) {
|
|
1777
|
-
const buffer = await
|
|
2691
|
+
const buffer = await fs5.readFile(filePath);
|
|
1778
2692
|
return "sha256:" + crypto2.createHash("sha256").update(buffer).digest("hex");
|
|
1779
2693
|
}
|
|
1780
2694
|
/**
|
|
@@ -1783,42 +2697,47 @@ var UninstallEngine = class {
|
|
|
1783
2697
|
* AGENTS.md bridge (agentsmd profile) is a plain markdown index with no
|
|
1784
2698
|
* frontmatter, so the marker is checked against the whole content.
|
|
1785
2699
|
*/
|
|
1786
|
-
async isManagedProjection(absPath, isAgentsMd) {
|
|
1787
|
-
const content = await
|
|
2700
|
+
async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
2701
|
+
const content = await fs5.readFile(absPath, "utf8");
|
|
1788
2702
|
if (isAgentsMd) {
|
|
1789
2703
|
return content.includes("managed-by: agents-united");
|
|
1790
2704
|
}
|
|
1791
|
-
|
|
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;
|
|
1792
2711
|
}
|
|
1793
2712
|
/**
|
|
1794
2713
|
* Removes empty projection dirs left behind after deleting a projected file
|
|
1795
2714
|
* (e.g. `.claude/agents/` and then `.claude/`). Only ever removes empty dirs.
|
|
1796
2715
|
*/
|
|
1797
2716
|
async removeEmptyProjectionDirs(workspaceRoot, projPath) {
|
|
1798
|
-
let dir =
|
|
2717
|
+
let dir = path7.dirname(path7.join(workspaceRoot, projPath));
|
|
1799
2718
|
while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {
|
|
1800
2719
|
try {
|
|
1801
|
-
const entries = await
|
|
2720
|
+
const entries = await fs5.readdir(dir);
|
|
1802
2721
|
if (entries.length > 0) break;
|
|
1803
|
-
await
|
|
2722
|
+
await fs5.rmdir(dir);
|
|
1804
2723
|
} catch {
|
|
1805
2724
|
break;
|
|
1806
2725
|
}
|
|
1807
|
-
dir =
|
|
2726
|
+
dir = path7.dirname(dir);
|
|
1808
2727
|
}
|
|
1809
2728
|
}
|
|
1810
2729
|
/**
|
|
1811
2730
|
* Removes every recorded projection for a canonical asset, guarding against
|
|
1812
2731
|
* clobbering user-modified files. Mirrors the hash-conflict error pattern.
|
|
1813
2732
|
*/
|
|
1814
|
-
async removeProjections(workspaceRoot, projectedTo, force) {
|
|
2733
|
+
async removeProjections(workspaceRoot, projectedTo, force, projectionHashes) {
|
|
1815
2734
|
for (const projPath of projectedTo) {
|
|
1816
|
-
const absProjection =
|
|
1817
|
-
if (!await
|
|
2735
|
+
const absProjection = path7.join(workspaceRoot, projPath);
|
|
2736
|
+
if (!await fs5.pathExists(absProjection)) {
|
|
1818
2737
|
continue;
|
|
1819
2738
|
}
|
|
1820
2739
|
const isAgentsMd = projPath === "AGENTS.md";
|
|
1821
|
-
const managed = await this.isManagedProjection(absProjection, isAgentsMd);
|
|
2740
|
+
const managed = await this.isManagedProjection(absProjection, isAgentsMd, projectionHashes?.[projPath]?.hash);
|
|
1822
2741
|
if (!managed && !force) {
|
|
1823
2742
|
throw new Error(
|
|
1824
2743
|
`Projection ${projPath} has user modifications. Use --force to remove.`
|
|
@@ -1830,14 +2749,14 @@ var UninstallEngine = class {
|
|
|
1830
2749
|
}
|
|
1831
2750
|
async safeRemoveProjection(absPath) {
|
|
1832
2751
|
try {
|
|
1833
|
-
const lstat = await
|
|
2752
|
+
const lstat = await fs5.lstat(absPath);
|
|
1834
2753
|
if (lstat.isSymbolicLink()) {
|
|
1835
|
-
await
|
|
2754
|
+
await fs5.unlink(absPath);
|
|
1836
2755
|
return;
|
|
1837
2756
|
}
|
|
1838
2757
|
} catch {
|
|
1839
2758
|
}
|
|
1840
|
-
await
|
|
2759
|
+
await fs5.remove(absPath);
|
|
1841
2760
|
}
|
|
1842
2761
|
/**
|
|
1843
2762
|
* Installed-addon freshness (plan 003): after a child bundle (one with a
|
|
@@ -1866,9 +2785,9 @@ var UninstallEngine = class {
|
|
|
1866
2785
|
for (const artifact of projection) {
|
|
1867
2786
|
if (artifact.kind !== "rule" && artifact.kind !== "team-manifest") continue;
|
|
1868
2787
|
if (artifact.content === void 0) continue;
|
|
1869
|
-
const dest =
|
|
1870
|
-
if (!await
|
|
1871
|
-
await
|
|
2788
|
+
const dest = path7.join(workspaceRoot, artifact.relPath);
|
|
2789
|
+
if (!await fs5.pathExists(dest)) continue;
|
|
2790
|
+
await fs5.writeFile(dest, artifact.content, "utf8");
|
|
1872
2791
|
const hash = await this.calculateHash(dest);
|
|
1873
2792
|
if (lockfile.projections?.[artifact.relPath]) {
|
|
1874
2793
|
lockfile.projections[artifact.relPath].hash = hash;
|
|
@@ -1890,15 +2809,15 @@ var UninstallEngine = class {
|
|
|
1890
2809
|
return options.scope || "project";
|
|
1891
2810
|
}
|
|
1892
2811
|
async safeRemove(targetDir, relPath) {
|
|
1893
|
-
const fullPath =
|
|
1894
|
-
if (relPath.startsWith("skills" +
|
|
2812
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
2813
|
+
if (relPath.startsWith("skills" + path7.sep) || relPath.startsWith("skills/")) {
|
|
1895
2814
|
const parts = relPath.split(/[/\\]/);
|
|
1896
2815
|
if (parts.length >= 2) {
|
|
1897
|
-
const skillDir =
|
|
2816
|
+
const skillDir = path7.join(targetDir, parts[0], parts[1]);
|
|
1898
2817
|
try {
|
|
1899
|
-
const lstat = await
|
|
2818
|
+
const lstat = await fs5.lstat(skillDir);
|
|
1900
2819
|
if (lstat.isSymbolicLink()) {
|
|
1901
|
-
await
|
|
2820
|
+
await fs5.unlink(skillDir);
|
|
1902
2821
|
return;
|
|
1903
2822
|
}
|
|
1904
2823
|
} catch {
|
|
@@ -1906,14 +2825,14 @@ var UninstallEngine = class {
|
|
|
1906
2825
|
}
|
|
1907
2826
|
}
|
|
1908
2827
|
try {
|
|
1909
|
-
const lstat = await
|
|
2828
|
+
const lstat = await fs5.lstat(fullPath);
|
|
1910
2829
|
if (lstat.isSymbolicLink()) {
|
|
1911
|
-
await
|
|
2830
|
+
await fs5.unlink(fullPath);
|
|
1912
2831
|
return;
|
|
1913
2832
|
}
|
|
1914
2833
|
} catch {
|
|
1915
2834
|
}
|
|
1916
|
-
await
|
|
2835
|
+
await fs5.remove(fullPath);
|
|
1917
2836
|
}
|
|
1918
2837
|
async uninstall(identifier, options = {}) {
|
|
1919
2838
|
const scope = this.parseScope(options);
|
|
@@ -1921,30 +2840,92 @@ var UninstallEngine = class {
|
|
|
1921
2840
|
const targetDirs = hosts.map((h) => AgentHostAdapter.resolveHostDir(scope, h, options.targetDir));
|
|
1922
2841
|
const resolved = await this.registry.resolve(identifier).catch(() => null);
|
|
1923
2842
|
const totalRemoved = [];
|
|
2843
|
+
const totalKept = [];
|
|
2844
|
+
const totalStale = [];
|
|
2845
|
+
const retainedOwners = /* @__PURE__ */ new Set();
|
|
1924
2846
|
for (const targetDir of targetDirs) {
|
|
1925
2847
|
const subPaths = AgentHostAdapter.getSubPaths(targetDir);
|
|
1926
|
-
if (!await
|
|
2848
|
+
if (!await fs5.pathExists(subPaths.lockfile)) {
|
|
1927
2849
|
continue;
|
|
1928
2850
|
}
|
|
1929
|
-
const lockfile = await
|
|
2851
|
+
const lockfile = await fs5.readJson(subPaths.lockfile);
|
|
1930
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
|
+
};
|
|
1931
2900
|
if (resolved && resolved.targetBundle) {
|
|
1932
2901
|
const bundleName = resolved.targetBundle;
|
|
1933
|
-
|
|
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) {
|
|
1934
2904
|
if (!options.dryRun) {
|
|
1935
|
-
const workspaceRoot =
|
|
1936
|
-
const ownedFiles = Object.entries(lockfile.files).filter(([, m]) => (m
|
|
1937
|
-
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;
|
|
1938
2908
|
if (ownedFiles.length === 0 && ownedProjections === 0) {
|
|
1939
2909
|
throw new Error(`No installed assets found matching "${bundleName}".`);
|
|
1940
2910
|
}
|
|
1941
2911
|
if (lockfile.projections) {
|
|
1942
2912
|
for (const [projRelPath, proj] of Object.entries(lockfile.projections)) {
|
|
1943
|
-
|
|
1944
|
-
|
|
2913
|
+
const owners = projectionOwners(projRelPath, proj);
|
|
2914
|
+
if (owners.includes(bundleName)) {
|
|
2915
|
+
proj.owners = owners.filter((o) => o !== bundleName);
|
|
1945
2916
|
if (proj.owners.length === 0) {
|
|
1946
|
-
const
|
|
1947
|
-
|
|
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) {
|
|
1948
2929
|
if (proj.managedMarker) {
|
|
1949
2930
|
const managed = await this.isManagedProjection(absProjection, projRelPath === "AGENTS.md");
|
|
1950
2931
|
if (!managed && !options.force) {
|
|
@@ -1960,20 +2941,36 @@ var UninstallEngine = class {
|
|
|
1960
2941
|
await this.removeEmptyProjectionDirs(workspaceRoot, projRelPath);
|
|
1961
2942
|
}
|
|
1962
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));
|
|
1963
2960
|
}
|
|
1964
2961
|
}
|
|
1965
2962
|
}
|
|
1966
2963
|
}
|
|
1967
2964
|
for (const [relPath, assetMeta] of Object.entries(lockfile.files)) {
|
|
1968
|
-
const
|
|
1969
|
-
if (!
|
|
1970
|
-
const newOwners =
|
|
2965
|
+
const recordOwners = assetOwners(assetMeta);
|
|
2966
|
+
if (!recordOwners.includes(bundleName)) continue;
|
|
2967
|
+
const newOwners = recordOwners.filter((o) => o !== bundleName);
|
|
1971
2968
|
if (newOwners.length === 0) {
|
|
1972
2969
|
if (assetMeta.projectedTo && assetMeta.projectedTo.length > 0) {
|
|
1973
|
-
await this.removeProjections(workspaceRoot, assetMeta.projectedTo, options.force);
|
|
2970
|
+
await this.removeProjections(workspaceRoot, assetMeta.projectedTo, options.force, lockfile.projections);
|
|
1974
2971
|
}
|
|
1975
|
-
const fullPath =
|
|
1976
|
-
if (await
|
|
2972
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
2973
|
+
if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
|
|
1977
2974
|
if (!options.force && assetMeta.method !== "symlink") {
|
|
1978
2975
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
1979
2976
|
if (currentHash && currentHash !== assetMeta.hash) {
|
|
@@ -1986,6 +2983,16 @@ var UninstallEngine = class {
|
|
|
1986
2983
|
delete lockfile.files[relPath];
|
|
1987
2984
|
} else {
|
|
1988
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
|
+
}
|
|
1989
2996
|
}
|
|
1990
2997
|
}
|
|
1991
2998
|
lockfile.installed.bundles = lockfile.installed.bundles.filter((b) => b !== bundleName);
|
|
@@ -1996,7 +3003,12 @@ var UninstallEngine = class {
|
|
|
1996
3003
|
const survival = /* @__PURE__ */ new Set();
|
|
1997
3004
|
for (const b of surviving) {
|
|
1998
3005
|
const bdef = await this.registry.getBundle(b);
|
|
1999
|
-
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
|
+
}
|
|
2000
3012
|
if (bdef.orchestrator) survival.add(`agents/${bdef.orchestrator}`);
|
|
2001
3013
|
(bdef.agents || []).forEach((a) => survival.add(`agents/${a}`));
|
|
2002
3014
|
(bdef.skills || []).forEach((s) => survival.add(`skills/${s}/SKILL.md`));
|
|
@@ -2008,17 +3020,30 @@ var UninstallEngine = class {
|
|
|
2008
3020
|
lockfile.installed.workflows = lockfile.installed.workflows.filter((w) => survival.has(`workflows/${w}`));
|
|
2009
3021
|
}
|
|
2010
3022
|
await this.refreshParentCoordination(bundleName, workspaceRoot, lockfile, scope);
|
|
2011
|
-
await
|
|
3023
|
+
await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
2012
3024
|
} else {
|
|
2013
|
-
|
|
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
|
+
}
|
|
2014
3039
|
}
|
|
2015
3040
|
}
|
|
2016
3041
|
} else {
|
|
2017
3042
|
const itemTargetRelPaths = Object.keys(lockfile.files).filter((p) => p.includes(identifier));
|
|
2018
3043
|
if (!options.dryRun) {
|
|
2019
3044
|
for (const relPath of itemTargetRelPaths) {
|
|
2020
|
-
const fullPath =
|
|
2021
|
-
if (await
|
|
3045
|
+
const fullPath = path7.join(targetDir, relPath);
|
|
3046
|
+
if (await fs5.pathExists(fullPath) || await fs5.pathExists(fullPath).catch(() => false)) {
|
|
2022
3047
|
if (!options.force && lockfile.files[relPath]?.method !== "symlink") {
|
|
2023
3048
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
2024
3049
|
const assetMeta = lockfile.files[relPath];
|
|
@@ -2031,24 +3056,33 @@ var UninstallEngine = class {
|
|
|
2031
3056
|
}
|
|
2032
3057
|
delete lockfile.files[relPath];
|
|
2033
3058
|
}
|
|
2034
|
-
await
|
|
3059
|
+
await fs5.writeJson(subPaths.lockfile, lockfile, { spaces: 2 });
|
|
2035
3060
|
} else {
|
|
2036
3061
|
removedFiles.push(...itemTargetRelPaths);
|
|
2037
3062
|
}
|
|
2038
3063
|
}
|
|
2039
3064
|
totalRemoved.push(...removedFiles);
|
|
3065
|
+
totalKept.push(...keptFiles);
|
|
3066
|
+
totalStale.push(...staleRecords);
|
|
2040
3067
|
}
|
|
2041
|
-
if (totalRemoved.length === 0 && !options.dryRun) {
|
|
3068
|
+
if (totalRemoved.length === 0 && totalKept.length === 0 && totalStale.length === 0 && !options.dryRun) {
|
|
2042
3069
|
throw new Error(`No installed assets found matching "${identifier}".`);
|
|
2043
3070
|
}
|
|
2044
|
-
return {
|
|
3071
|
+
return {
|
|
3072
|
+
removed: totalRemoved,
|
|
3073
|
+
kept: totalKept,
|
|
3074
|
+
staleRecords: totalStale,
|
|
3075
|
+
retainedOwners: [...retainedOwners].sort(),
|
|
3076
|
+
targetDirs,
|
|
3077
|
+
dryRun: options.dryRun || false
|
|
3078
|
+
};
|
|
2045
3079
|
}
|
|
2046
3080
|
};
|
|
2047
3081
|
|
|
2048
3082
|
// src/core/inventory.ts
|
|
2049
|
-
import
|
|
3083
|
+
import path8 from "path";
|
|
2050
3084
|
import os2 from "os";
|
|
2051
|
-
import
|
|
3085
|
+
import fs6 from "fs-extra";
|
|
2052
3086
|
var InventoryScanner = class _InventoryScanner {
|
|
2053
3087
|
registry;
|
|
2054
3088
|
constructor(registry2) {
|
|
@@ -2059,10 +3093,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
2059
3093
|
const cwd = process.cwd();
|
|
2060
3094
|
let prettyPath = targetDir;
|
|
2061
3095
|
if (targetDir.startsWith(cwd)) {
|
|
2062
|
-
const rel =
|
|
3096
|
+
const rel = path8.relative(cwd, targetDir);
|
|
2063
3097
|
prettyPath = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
2064
3098
|
} else if (targetDir.startsWith(home)) {
|
|
2065
|
-
const rel =
|
|
3099
|
+
const rel = path8.relative(home, targetDir);
|
|
2066
3100
|
prettyPath = `~/${rel.replace(/\\/g, "/")}`;
|
|
2067
3101
|
}
|
|
2068
3102
|
const fanoutSuffix = fanout && fanout.length > 0 ? ` + ${fanout.join(", ")}` : "";
|
|
@@ -2092,7 +3126,7 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
2092
3126
|
const candidateDirs = [];
|
|
2093
3127
|
if (options.targetDir) {
|
|
2094
3128
|
candidateDirs.push({
|
|
2095
|
-
dir:
|
|
3129
|
+
dir: path8.resolve(options.targetDir),
|
|
2096
3130
|
scope: options.scope || "project",
|
|
2097
3131
|
host: hosts[0] || "agents"
|
|
2098
3132
|
});
|
|
@@ -2114,10 +3148,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
2114
3148
|
const canonicalDir = AgentHostAdapter.resolveHostDir(scope, "agents", options.targetDir);
|
|
2115
3149
|
let displayLoc = canonicalDir;
|
|
2116
3150
|
if (canonicalDir.startsWith(cwd)) {
|
|
2117
|
-
const rel =
|
|
3151
|
+
const rel = path8.relative(cwd, canonicalDir);
|
|
2118
3152
|
displayLoc = rel ? `./${rel.replace(/\\/g, "/")}` : ".";
|
|
2119
3153
|
} else if (canonicalDir.startsWith(home)) {
|
|
2120
|
-
const rel =
|
|
3154
|
+
const rel = path8.relative(home, canonicalDir);
|
|
2121
3155
|
displayLoc = `~/${rel.replace(/\\/g, "/")}`;
|
|
2122
3156
|
}
|
|
2123
3157
|
scannedLocationsMap.set(`${scope}:${canonicalDir}`, {
|
|
@@ -2129,14 +3163,14 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
2129
3163
|
});
|
|
2130
3164
|
}
|
|
2131
3165
|
for (const item of candidateDirs) {
|
|
2132
|
-
const lockfilePath =
|
|
2133
|
-
if (!await
|
|
3166
|
+
const lockfilePath = path8.join(item.dir, "agents-united.json");
|
|
3167
|
+
if (!await fs6.pathExists(lockfilePath)) {
|
|
2134
3168
|
continue;
|
|
2135
3169
|
}
|
|
2136
3170
|
scannedTargetDirs.add(item.dir);
|
|
2137
3171
|
let lockfile;
|
|
2138
3172
|
try {
|
|
2139
|
-
lockfile = await
|
|
3173
|
+
lockfile = await fs6.readJson(lockfilePath);
|
|
2140
3174
|
} catch {
|
|
2141
3175
|
continue;
|
|
2142
3176
|
}
|
|
@@ -2285,10 +3319,10 @@ var InventoryScanner = class _InventoryScanner {
|
|
|
2285
3319
|
};
|
|
2286
3320
|
|
|
2287
3321
|
// src/core/updater.ts
|
|
2288
|
-
import
|
|
3322
|
+
import path9 from "path";
|
|
2289
3323
|
import crypto3 from "crypto";
|
|
2290
|
-
import
|
|
2291
|
-
var UpdateEngine = class {
|
|
3324
|
+
import fs7 from "fs-extra";
|
|
3325
|
+
var UpdateEngine = class _UpdateEngine {
|
|
2292
3326
|
registry;
|
|
2293
3327
|
scanner;
|
|
2294
3328
|
installer;
|
|
@@ -2310,7 +3344,7 @@ var UpdateEngine = class {
|
|
|
2310
3344
|
return deduplicated;
|
|
2311
3345
|
}
|
|
2312
3346
|
async calculateHash(filePath) {
|
|
2313
|
-
const buffer = await
|
|
3347
|
+
const buffer = await fs7.readFile(filePath);
|
|
2314
3348
|
return "sha256:" + crypto3.createHash("sha256").update(buffer).digest("hex");
|
|
2315
3349
|
}
|
|
2316
3350
|
/**
|
|
@@ -2318,26 +3352,31 @@ var UpdateEngine = class {
|
|
|
2318
3352
|
* M5). Agent projections place the marker as the first body line after frontmatter;
|
|
2319
3353
|
* the AGENTS.md bridge has no frontmatter so it is checked over the whole content.
|
|
2320
3354
|
*/
|
|
2321
|
-
async isManagedProjection(absPath, isAgentsMd) {
|
|
2322
|
-
const content = await
|
|
3355
|
+
async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
3356
|
+
const content = await fs7.readFile(absPath, "utf8");
|
|
2323
3357
|
if (isAgentsMd) {
|
|
2324
3358
|
return content.includes("managed-by: agents-united");
|
|
2325
3359
|
}
|
|
2326
|
-
|
|
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;
|
|
2327
3366
|
}
|
|
2328
3367
|
/**
|
|
2329
3368
|
* Returns the first user-modified projection path belonging to a record, or '' when
|
|
2330
3369
|
* every recorded projection still carries the managed marker.
|
|
2331
3370
|
*/
|
|
2332
3371
|
async staleProjection(record, lockfile) {
|
|
2333
|
-
let workspaceRoot =
|
|
3372
|
+
let workspaceRoot = path9.dirname(record.targetDir);
|
|
2334
3373
|
for (const assetMeta of Object.values(lockfile.files)) {
|
|
2335
3374
|
if (assetMeta.bundle !== record.name) continue;
|
|
2336
3375
|
if (!assetMeta.projectedTo || assetMeta.projectedTo.length === 0) continue;
|
|
2337
3376
|
for (const projPath of assetMeta.projectedTo) {
|
|
2338
|
-
const absProjection =
|
|
2339
|
-
if (await
|
|
2340
|
-
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);
|
|
2341
3380
|
if (!managed) {
|
|
2342
3381
|
return projPath;
|
|
2343
3382
|
}
|
|
@@ -2346,6 +3385,79 @@ var UpdateEngine = class {
|
|
|
2346
3385
|
}
|
|
2347
3386
|
return "";
|
|
2348
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
|
+
}
|
|
2349
3461
|
async checkUpdates(options = {}) {
|
|
2350
3462
|
const inventory = await this.scanner.scan(options);
|
|
2351
3463
|
const items = [];
|
|
@@ -2397,8 +3509,8 @@ var UpdateEngine = class {
|
|
|
2397
3509
|
if (options.dryRun) {
|
|
2398
3510
|
const dryProjections = [];
|
|
2399
3511
|
for (const record of recordsToUpdate) {
|
|
2400
|
-
const lockfilePath =
|
|
2401
|
-
if (await
|
|
3512
|
+
const lockfilePath = path9.join(record.targetDir, "agents-united.json");
|
|
3513
|
+
if (await fs7.pathExists(lockfilePath)) {
|
|
2402
3514
|
const dryResult = await this.installer.install(record.name, {
|
|
2403
3515
|
scope: record.scope,
|
|
2404
3516
|
method: record.method,
|
|
@@ -2424,18 +3536,18 @@ var UpdateEngine = class {
|
|
|
2424
3536
|
const skipped = [];
|
|
2425
3537
|
const collectedProjections = [];
|
|
2426
3538
|
for (const record of recordsToUpdate) {
|
|
2427
|
-
const lockfilePath =
|
|
2428
|
-
if (!await
|
|
3539
|
+
const lockfilePath = path9.join(record.targetDir, "agents-united.json");
|
|
3540
|
+
if (!await fs7.pathExists(lockfilePath)) {
|
|
2429
3541
|
continue;
|
|
2430
3542
|
}
|
|
2431
|
-
const lockfile = await
|
|
3543
|
+
const lockfile = await fs7.readJson(lockfilePath);
|
|
2432
3544
|
if (record.method === "copy" && !options.force && lockfile.files) {
|
|
2433
3545
|
let hasConflict = false;
|
|
2434
3546
|
let conflictRelPath = "";
|
|
2435
3547
|
for (const [relPath, assetMeta] of Object.entries(lockfile.files)) {
|
|
2436
3548
|
if (assetMeta.bundle === record.name || relPath.includes(record.name)) {
|
|
2437
|
-
const fullPath =
|
|
2438
|
-
if (await
|
|
3549
|
+
const fullPath = path9.join(record.targetDir, relPath);
|
|
3550
|
+
if (await fs7.pathExists(fullPath)) {
|
|
2439
3551
|
const currentHash = await this.calculateHash(fullPath).catch(() => null);
|
|
2440
3552
|
if (currentHash && currentHash !== assetMeta.hash) {
|
|
2441
3553
|
hasConflict = true;
|
|
@@ -2463,6 +3575,13 @@ var UpdateEngine = class {
|
|
|
2463
3575
|
continue;
|
|
2464
3576
|
}
|
|
2465
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
|
+
}
|
|
2466
3585
|
const installResult = await this.installer.install(record.name, {
|
|
2467
3586
|
scope: record.scope,
|
|
2468
3587
|
method: record.method,
|
|
@@ -2475,11 +3594,11 @@ var UpdateEngine = class {
|
|
|
2475
3594
|
if (installResult.projections) {
|
|
2476
3595
|
collectedProjections.push(...installResult.projections);
|
|
2477
3596
|
}
|
|
2478
|
-
if (await
|
|
2479
|
-
const updatedLockfile = await
|
|
3597
|
+
if (await fs7.pathExists(lockfilePath)) {
|
|
3598
|
+
const updatedLockfile = await fs7.readJson(lockfilePath);
|
|
2480
3599
|
updatedLockfile.bundleVersions = updatedLockfile.bundleVersions || {};
|
|
2481
3600
|
updatedLockfile.bundleVersions[record.name] = record.upstreamVersion;
|
|
2482
|
-
await
|
|
3601
|
+
await fs7.writeJson(lockfilePath, updatedLockfile, { spaces: 2 });
|
|
2483
3602
|
if (updatedLockfile.fanout) {
|
|
2484
3603
|
record.fanout = updatedLockfile.fanout;
|
|
2485
3604
|
record.projections = updatedLockfile.fanout;
|
|
@@ -2506,14 +3625,18 @@ var UpdateEngine = class {
|
|
|
2506
3625
|
};
|
|
2507
3626
|
|
|
2508
3627
|
// src/core/doctor.ts
|
|
2509
|
-
import
|
|
2510
|
-
import
|
|
3628
|
+
import path12 from "path";
|
|
3629
|
+
import fs10 from "fs-extra";
|
|
2511
3630
|
import crypto4 from "crypto";
|
|
2512
3631
|
import YAML3 from "yaml";
|
|
2513
3632
|
|
|
3633
|
+
// src/core/claude-capabilities.ts
|
|
3634
|
+
import path11 from "path";
|
|
3635
|
+
import fs9 from "fs-extra";
|
|
3636
|
+
|
|
2514
3637
|
// src/core/cline-capabilities.ts
|
|
2515
|
-
import
|
|
2516
|
-
import
|
|
3638
|
+
import path10 from "path";
|
|
3639
|
+
import fs8 from "fs-extra";
|
|
2517
3640
|
import { execFile } from "child_process";
|
|
2518
3641
|
var defaultProcessRunner = (executable, args, options) => {
|
|
2519
3642
|
return new Promise((resolve) => {
|
|
@@ -2555,7 +3678,165 @@ var defaultProcessRunner = (executable, args, options) => {
|
|
|
2555
3678
|
}
|
|
2556
3679
|
});
|
|
2557
3680
|
};
|
|
2558
|
-
var ClineCapabilityProbe = class {
|
|
3681
|
+
var ClineCapabilityProbe = class {
|
|
3682
|
+
runner;
|
|
3683
|
+
customResolver;
|
|
3684
|
+
constructor(runner, options) {
|
|
3685
|
+
this.runner = runner || defaultProcessRunner;
|
|
3686
|
+
this.customResolver = options?.resolveExecutable;
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Safe executable resolution across POSIX and Windows layouts.
|
|
3690
|
+
*/
|
|
3691
|
+
resolveExecutable() {
|
|
3692
|
+
if (this.customResolver) {
|
|
3693
|
+
return this.customResolver();
|
|
3694
|
+
}
|
|
3695
|
+
const envBin = process.env.CLINE_BIN_PATH;
|
|
3696
|
+
if (envBin && path10.isAbsolute(envBin) && fs8.existsSync(envBin)) {
|
|
3697
|
+
return {
|
|
3698
|
+
executable: envBin,
|
|
3699
|
+
prefixArgs: [],
|
|
3700
|
+
source: "env-binary"
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
const pathEnv = process.env.PATH || "";
|
|
3704
|
+
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
3705
|
+
const dirs = pathEnv.split(pathSeparator).filter(Boolean);
|
|
3706
|
+
if (process.platform === "win32") {
|
|
3707
|
+
const exeNames = ["cline.cmd", "cline.bat", "cline.exe", "cline"];
|
|
3708
|
+
for (const dir of dirs) {
|
|
3709
|
+
for (const exeName of exeNames) {
|
|
3710
|
+
const fullPath = path10.join(dir, exeName);
|
|
3711
|
+
if (fs8.existsSync(fullPath)) {
|
|
3712
|
+
const adjacentWrapper1 = path10.join(dir, "node_modules", "cline", "bin", "cline");
|
|
3713
|
+
const adjacentWrapper2 = path10.join(dir, "node_modules", "@cline", "cline", "bin", "cline");
|
|
3714
|
+
if (fs8.existsSync(adjacentWrapper1)) {
|
|
3715
|
+
return {
|
|
3716
|
+
executable: process.execPath,
|
|
3717
|
+
prefixArgs: [adjacentWrapper1],
|
|
3718
|
+
source: "node-wrapper"
|
|
3719
|
+
};
|
|
3720
|
+
}
|
|
3721
|
+
if (fs8.existsSync(adjacentWrapper2)) {
|
|
3722
|
+
return {
|
|
3723
|
+
executable: process.execPath,
|
|
3724
|
+
prefixArgs: [adjacentWrapper2],
|
|
3725
|
+
source: "node-wrapper"
|
|
3726
|
+
};
|
|
3727
|
+
}
|
|
3728
|
+
const ext = path10.extname(fullPath).toLowerCase();
|
|
3729
|
+
if (ext === ".cmd" || ext === ".bat") {
|
|
3730
|
+
return {
|
|
3731
|
+
executable: "cmd.exe",
|
|
3732
|
+
prefixArgs: ["/c", fullPath],
|
|
3733
|
+
source: "path-executable"
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
3736
|
+
return {
|
|
3737
|
+
executable: fullPath,
|
|
3738
|
+
prefixArgs: [],
|
|
3739
|
+
source: "path-executable"
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
} else {
|
|
3745
|
+
for (const dir of dirs) {
|
|
3746
|
+
const fullPath = path10.join(dir, "cline");
|
|
3747
|
+
if (fs8.existsSync(fullPath)) {
|
|
3748
|
+
return {
|
|
3749
|
+
executable: fullPath,
|
|
3750
|
+
prefixArgs: [],
|
|
3751
|
+
source: "path-executable"
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
return null;
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Run read-only capability probe against local Cline.
|
|
3760
|
+
*/
|
|
3761
|
+
async probe() {
|
|
3762
|
+
const cmd = this.resolveExecutable();
|
|
3763
|
+
const diagnostics = [];
|
|
3764
|
+
if (!cmd) {
|
|
3765
|
+
diagnostics.push("Cline executable not found on PATH or via CLINE_BIN_PATH.");
|
|
3766
|
+
return {
|
|
3767
|
+
installed: false,
|
|
3768
|
+
namedTeams: false,
|
|
3769
|
+
rolePresetConsumer: "unknown",
|
|
3770
|
+
diagnostics
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
let version;
|
|
3774
|
+
try {
|
|
3775
|
+
const verRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "version"], {
|
|
3776
|
+
timeoutMs: 5e3
|
|
3777
|
+
});
|
|
3778
|
+
if (verRes.exitCode === 0) {
|
|
3779
|
+
version = verRes.stdout.trim().split("\n")[0]?.trim();
|
|
3780
|
+
} else {
|
|
3781
|
+
diagnostics.push(`Cline version check failed (exit code ${verRes.exitCode}): ${verRes.stderr.trim()}`);
|
|
3782
|
+
return {
|
|
3783
|
+
installed: false,
|
|
3784
|
+
command: cmd,
|
|
3785
|
+
namedTeams: false,
|
|
3786
|
+
rolePresetConsumer: "unknown",
|
|
3787
|
+
diagnostics
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
} catch (err) {
|
|
3791
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3792
|
+
diagnostics.push(`Error executing Cline version check: ${msg}`);
|
|
3793
|
+
return {
|
|
3794
|
+
installed: false,
|
|
3795
|
+
command: cmd,
|
|
3796
|
+
namedTeams: false,
|
|
3797
|
+
rolePresetConsumer: "unknown",
|
|
3798
|
+
diagnostics
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3801
|
+
let namedTeams = false;
|
|
3802
|
+
try {
|
|
3803
|
+
const probeRes = await this.runner(
|
|
3804
|
+
cmd.executable,
|
|
3805
|
+
[...cmd.prefixArgs, "--team-name", "agents-united-capability-probe", "version"],
|
|
3806
|
+
{ timeoutMs: 5e3 }
|
|
3807
|
+
);
|
|
3808
|
+
if (probeRes.exitCode === 0) {
|
|
3809
|
+
namedTeams = true;
|
|
3810
|
+
} else {
|
|
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 {
|
|
2559
3840
|
runner;
|
|
2560
3841
|
customResolver;
|
|
2561
3842
|
constructor(runner, options) {
|
|
@@ -2569,8 +3850,8 @@ var ClineCapabilityProbe = class {
|
|
|
2569
3850
|
if (this.customResolver) {
|
|
2570
3851
|
return this.customResolver();
|
|
2571
3852
|
}
|
|
2572
|
-
const envBin = process.env.
|
|
2573
|
-
if (envBin &&
|
|
3853
|
+
const envBin = process.env.CLAUDE_BIN_PATH;
|
|
3854
|
+
if (envBin && path11.isAbsolute(envBin) && fs9.existsSync(envBin)) {
|
|
2574
3855
|
return {
|
|
2575
3856
|
executable: envBin,
|
|
2576
3857
|
prefixArgs: [],
|
|
@@ -2581,28 +3862,28 @@ var ClineCapabilityProbe = class {
|
|
|
2581
3862
|
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
|
2582
3863
|
const dirs = pathEnv.split(pathSeparator).filter(Boolean);
|
|
2583
3864
|
if (process.platform === "win32") {
|
|
2584
|
-
const exeNames = ["
|
|
3865
|
+
const exeNames = ["claude.cmd", "claude.bat", "claude.exe", "claude"];
|
|
2585
3866
|
for (const dir of dirs) {
|
|
2586
3867
|
for (const exeName of exeNames) {
|
|
2587
|
-
const fullPath =
|
|
2588
|
-
if (
|
|
2589
|
-
const adjacentWrapper1 =
|
|
2590
|
-
const adjacentWrapper2 =
|
|
2591
|
-
if (
|
|
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)) {
|
|
2592
3873
|
return {
|
|
2593
3874
|
executable: process.execPath,
|
|
2594
3875
|
prefixArgs: [adjacentWrapper1],
|
|
2595
3876
|
source: "node-wrapper"
|
|
2596
3877
|
};
|
|
2597
3878
|
}
|
|
2598
|
-
if (
|
|
3879
|
+
if (fs9.existsSync(adjacentWrapper2)) {
|
|
2599
3880
|
return {
|
|
2600
3881
|
executable: process.execPath,
|
|
2601
3882
|
prefixArgs: [adjacentWrapper2],
|
|
2602
3883
|
source: "node-wrapper"
|
|
2603
3884
|
};
|
|
2604
3885
|
}
|
|
2605
|
-
const ext =
|
|
3886
|
+
const ext = path11.extname(fullPath).toLowerCase();
|
|
2606
3887
|
if (ext === ".cmd" || ext === ".bat") {
|
|
2607
3888
|
return {
|
|
2608
3889
|
executable: "cmd.exe",
|
|
@@ -2620,8 +3901,8 @@ var ClineCapabilityProbe = class {
|
|
|
2620
3901
|
}
|
|
2621
3902
|
} else {
|
|
2622
3903
|
for (const dir of dirs) {
|
|
2623
|
-
const fullPath =
|
|
2624
|
-
if (
|
|
3904
|
+
const fullPath = path11.join(dir, "claude");
|
|
3905
|
+
if (fs9.existsSync(fullPath)) {
|
|
2625
3906
|
return {
|
|
2626
3907
|
executable: fullPath,
|
|
2627
3908
|
prefixArgs: [],
|
|
@@ -2633,90 +3914,134 @@ var ClineCapabilityProbe = class {
|
|
|
2633
3914
|
return null;
|
|
2634
3915
|
}
|
|
2635
3916
|
/**
|
|
2636
|
-
* Run read-only capability probe against local
|
|
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.
|
|
2637
3919
|
*/
|
|
2638
3920
|
async probe() {
|
|
2639
3921
|
const cmd = this.resolveExecutable();
|
|
2640
3922
|
const diagnostics = [];
|
|
2641
3923
|
if (!cmd) {
|
|
2642
|
-
diagnostics.push("
|
|
3924
|
+
diagnostics.push("Claude Code executable not found on PATH or via CLAUDE_BIN_PATH.");
|
|
2643
3925
|
return {
|
|
2644
3926
|
installed: false,
|
|
2645
|
-
|
|
2646
|
-
|
|
3927
|
+
pluginSupport: false,
|
|
3928
|
+
agentTeamsExperimental: false,
|
|
3929
|
+
subagentHandback: false,
|
|
2647
3930
|
diagnostics
|
|
2648
3931
|
};
|
|
2649
3932
|
}
|
|
2650
3933
|
let version;
|
|
3934
|
+
let subagentHandback = false;
|
|
2651
3935
|
try {
|
|
2652
|
-
const verRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "version"], {
|
|
3936
|
+
const verRes = await this.runner(cmd.executable, [...cmd.prefixArgs, "--version"], {
|
|
2653
3937
|
timeoutMs: 5e3
|
|
2654
3938
|
});
|
|
2655
3939
|
if (verRes.exitCode === 0) {
|
|
2656
3940
|
version = verRes.stdout.trim().split("\n")[0]?.trim();
|
|
3941
|
+
subagentHandback = supportsSubagentHandback(version);
|
|
2657
3942
|
} else {
|
|
2658
|
-
diagnostics.push(`
|
|
3943
|
+
diagnostics.push(`Claude Code version check failed (exit code ${verRes.exitCode}): ${verRes.stderr.trim()}`);
|
|
2659
3944
|
return {
|
|
2660
3945
|
installed: false,
|
|
2661
3946
|
command: cmd,
|
|
2662
|
-
|
|
2663
|
-
|
|
3947
|
+
pluginSupport: false,
|
|
3948
|
+
agentTeamsExperimental: false,
|
|
3949
|
+
subagentHandback: false,
|
|
2664
3950
|
diagnostics
|
|
2665
3951
|
};
|
|
2666
3952
|
}
|
|
2667
3953
|
} catch (err) {
|
|
2668
3954
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2669
|
-
diagnostics.push(`Error executing
|
|
3955
|
+
diagnostics.push(`Error executing Claude Code version check: ${msg}`);
|
|
2670
3956
|
return {
|
|
2671
3957
|
installed: false,
|
|
2672
3958
|
command: cmd,
|
|
2673
|
-
|
|
2674
|
-
|
|
3959
|
+
pluginSupport: false,
|
|
3960
|
+
agentTeamsExperimental: false,
|
|
3961
|
+
subagentHandback: false,
|
|
2675
3962
|
diagnostics
|
|
2676
3963
|
};
|
|
2677
3964
|
}
|
|
2678
|
-
let
|
|
3965
|
+
let pluginSupport = false;
|
|
3966
|
+
let agentTeamsExperimental = false;
|
|
2679
3967
|
try {
|
|
2680
|
-
const
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
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");
|
|
2687
3975
|
} else {
|
|
2688
|
-
diagnostics.push(
|
|
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
|
+
);
|
|
2689
3979
|
}
|
|
2690
3980
|
} catch (err) {
|
|
2691
3981
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2692
|
-
diagnostics.push(`
|
|
3982
|
+
diagnostics.push(`Claude Code help probe failed: ${msg}`);
|
|
2693
3983
|
}
|
|
2694
3984
|
return {
|
|
2695
3985
|
installed: true,
|
|
2696
3986
|
version,
|
|
2697
3987
|
command: cmd,
|
|
2698
|
-
|
|
2699
|
-
|
|
3988
|
+
pluginSupport,
|
|
3989
|
+
agentTeamsExperimental,
|
|
3990
|
+
subagentHandback,
|
|
2700
3991
|
diagnostics
|
|
2701
3992
|
};
|
|
2702
3993
|
}
|
|
2703
3994
|
};
|
|
2704
3995
|
|
|
2705
3996
|
// src/core/doctor.ts
|
|
2706
|
-
var DoctorEngine = class {
|
|
2707
|
-
static async isManagedProjection(absPath, isAgentsMd) {
|
|
2708
|
-
const content = await
|
|
3997
|
+
var DoctorEngine = class _DoctorEngine {
|
|
3998
|
+
static async isManagedProjection(absPath, isAgentsMd, recordedHash) {
|
|
3999
|
+
const content = await fs10.readFile(absPath, "utf8");
|
|
2709
4000
|
if (isAgentsMd) {
|
|
2710
4001
|
return content.includes("managed-by: agents-united");
|
|
2711
4002
|
}
|
|
2712
|
-
|
|
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;
|
|
2713
4032
|
}
|
|
2714
4033
|
/**
|
|
2715
4034
|
* ADR 0017 — re-render the compound projections owned by the installed bundles so
|
|
2716
4035
|
* doctor can compare what is on disk with what the current renderer produces.
|
|
2717
4036
|
*
|
|
2718
|
-
* Returns projection path -> acceptable content variants
|
|
2719
|
-
* is unavailable (the freshness check is then skipped rather than
|
|
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.
|
|
2720
4045
|
*
|
|
2721
4046
|
* Two variants are produced per path on purpose: the coordinator rule and team manifest
|
|
2722
4047
|
* legitimately render either with or without the installed-bundle addon exclusions
|
|
@@ -2724,7 +4049,7 @@ var DoctorEngine = class {
|
|
|
2724
4049
|
* projection is fresh, which keeps the check false-positive free while still catching
|
|
2725
4050
|
* content that matches neither.
|
|
2726
4051
|
*/
|
|
2727
|
-
static async renderProjectionVariants(owners, installedBundles) {
|
|
4052
|
+
static async renderProjectionVariants(owners, installedBundles, host) {
|
|
2728
4053
|
if (owners.size === 0) return null;
|
|
2729
4054
|
let resolver;
|
|
2730
4055
|
try {
|
|
@@ -2741,7 +4066,13 @@ var DoctorEngine = class {
|
|
|
2741
4066
|
if (!bundleDef) continue;
|
|
2742
4067
|
const resolved = await resolver.resolve(bundleDef.name);
|
|
2743
4068
|
for (const excludeAddons of [void 0, installedBundles]) {
|
|
2744
|
-
const artifacts = await ClineProjector.planCompoundProjection(
|
|
4069
|
+
const artifacts = host === "cline" ? await ClineProjector.planCompoundProjection(
|
|
4070
|
+
bundleDef,
|
|
4071
|
+
"project",
|
|
4072
|
+
resolved,
|
|
4073
|
+
registryDir,
|
|
4074
|
+
excludeAddons
|
|
4075
|
+
) : await ClaudeProjector.planCompoundProjection(
|
|
2745
4076
|
bundleDef,
|
|
2746
4077
|
"project",
|
|
2747
4078
|
resolved,
|
|
@@ -2771,12 +4102,13 @@ var DoctorEngine = class {
|
|
|
2771
4102
|
let workflowsCount = 0;
|
|
2772
4103
|
let manifest;
|
|
2773
4104
|
let clineCapability;
|
|
4105
|
+
let claudeCapability;
|
|
2774
4106
|
let isInitialized = false;
|
|
2775
|
-
const lockfileExists = await
|
|
2776
|
-
const agentsDirExists = await
|
|
4107
|
+
const lockfileExists = await fs10.pathExists(subPaths.lockfile);
|
|
4108
|
+
const agentsDirExists = await fs10.pathExists(subPaths.agentsDir);
|
|
2777
4109
|
if (lockfileExists) {
|
|
2778
4110
|
try {
|
|
2779
|
-
const parsed = await
|
|
4111
|
+
const parsed = await fs10.readJson(subPaths.lockfile);
|
|
2780
4112
|
manifest = parsed;
|
|
2781
4113
|
agentsCount = parsed.installed?.agents?.length || 0;
|
|
2782
4114
|
skillsCount = parsed.installed?.skills?.length || 0;
|
|
@@ -2790,11 +4122,11 @@ var DoctorEngine = class {
|
|
|
2790
4122
|
} else if (agentsDirExists) {
|
|
2791
4123
|
warnings.push(`No lockfile found at ${subPaths.lockfile}, but agent files were detected in ${subPaths.agentsDir}. Run 'agents update' to sync.`);
|
|
2792
4124
|
}
|
|
2793
|
-
if (await
|
|
2794
|
-
const agentFiles = await
|
|
4125
|
+
if (await fs10.pathExists(subPaths.agentsDir)) {
|
|
4126
|
+
const agentFiles = await fs10.readdir(subPaths.agentsDir);
|
|
2795
4127
|
for (const file of agentFiles) {
|
|
2796
4128
|
if (file.endsWith(".md")) {
|
|
2797
|
-
const content = await
|
|
4129
|
+
const content = await fs10.readFile(path12.join(subPaths.agentsDir, file), "utf8");
|
|
2798
4130
|
const frontmatterMatch = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
|
|
2799
4131
|
if (!frontmatterMatch) {
|
|
2800
4132
|
warnings.push(`Agent ${file} is missing YAML frontmatter.`);
|
|
@@ -2811,79 +4143,104 @@ var DoctorEngine = class {
|
|
|
2811
4143
|
}
|
|
2812
4144
|
}
|
|
2813
4145
|
}
|
|
2814
|
-
if (await
|
|
2815
|
-
const remainingWorkflowFiles = (await
|
|
4146
|
+
if (await fs10.pathExists(subPaths.workflowsDir)) {
|
|
4147
|
+
const remainingWorkflowFiles = (await fs10.readdir(subPaths.workflowsDir)).filter((f) => f.endsWith(".md"));
|
|
2816
4148
|
if (remainingWorkflowFiles.length > 0) {
|
|
2817
4149
|
warnings.push(`Detected ${remainingWorkflowFiles.length} legacy workflows in ${subPaths.workflowsDir}. Run 'agents update' to automatically migrate them to modern skills.`);
|
|
2818
4150
|
}
|
|
2819
4151
|
}
|
|
2820
4152
|
if (manifest) {
|
|
2821
|
-
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
|
+
};
|
|
2822
4160
|
for (const [relPath, assetMeta] of Object.entries(manifest.files)) {
|
|
2823
4161
|
const projectedTo = assetMeta.projectedTo;
|
|
2824
4162
|
if (!projectedTo || projectedTo.length === 0) continue;
|
|
2825
4163
|
for (const projPath of projectedTo) {
|
|
2826
|
-
const absProjection =
|
|
2827
|
-
if (!await
|
|
2828
|
-
|
|
2829
|
-
for (const [projRelPath, proj] of Object.entries(manifest.projections ?? {})) {
|
|
2830
|
-
if (projRelPath === projPath) continue;
|
|
2831
|
-
if (proj.canonical !== relPath) continue;
|
|
2832
|
-
if (!await fs8.pathExists(path10.join(workspaceRoot, projRelPath))) continue;
|
|
2833
|
-
superseding = { relPath: projRelPath, host: proj.host, owners: proj.owners ?? [] };
|
|
2834
|
-
break;
|
|
2835
|
-
}
|
|
4164
|
+
const absProjection = path12.join(workspaceRoot, projPath);
|
|
4165
|
+
if (!await fs10.pathExists(absProjection)) {
|
|
4166
|
+
const superseding = await this.findSupersedingProjection(projPath, relPath, manifest, workspaceRoot);
|
|
2836
4167
|
if (superseding) {
|
|
2837
4168
|
const owner = superseding.owners[0];
|
|
2838
|
-
|
|
4169
|
+
pushProjectionWarning(
|
|
4170
|
+
projPath,
|
|
2839
4171
|
`Stale projection ${projPath} for canonical ${relPath} (superseded by ${superseding.relPath}).` + (owner ? ` Run: agents update ${owner} --fanout ${superseding.host} to reconcile.` : "")
|
|
2840
4172
|
);
|
|
2841
4173
|
} else {
|
|
2842
|
-
|
|
4174
|
+
pushProjectionWarning(
|
|
4175
|
+
projPath,
|
|
2843
4176
|
`Missing projection ${projPath} for canonical ${relPath}. Re-run: agents add ... --fanout`
|
|
2844
4177
|
);
|
|
2845
4178
|
}
|
|
2846
4179
|
continue;
|
|
2847
4180
|
}
|
|
2848
|
-
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md");
|
|
4181
|
+
const managed = await this.isManagedProjection(absProjection, projPath === "AGENTS.md", manifest.projections?.[projPath]?.hash);
|
|
2849
4182
|
if (!managed) {
|
|
2850
|
-
|
|
4183
|
+
pushProjectionWarning(projPath, `user-modified projection ${projPath}`);
|
|
2851
4184
|
}
|
|
2852
4185
|
}
|
|
2853
4186
|
}
|
|
2854
4187
|
if (manifest.projections) {
|
|
2855
4188
|
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
2856
|
-
const absPath =
|
|
2857
|
-
if (!await
|
|
2858
|
-
|
|
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
|
+
}
|
|
2859
4204
|
continue;
|
|
2860
4205
|
}
|
|
2861
4206
|
if (proj.managedMarker) {
|
|
2862
|
-
const managed = await this.isManagedProjection(absPath, projRelPath === "AGENTS.md");
|
|
4207
|
+
const managed = await this.isManagedProjection(absPath, projRelPath === "AGENTS.md", proj.hash);
|
|
2863
4208
|
if (!managed) {
|
|
2864
|
-
|
|
4209
|
+
pushProjectionWarning(projRelPath, `user-modified projection ${projRelPath}`);
|
|
2865
4210
|
}
|
|
2866
4211
|
}
|
|
2867
4212
|
}
|
|
2868
4213
|
}
|
|
2869
4214
|
if (manifest.projections && manifest.installed) {
|
|
2870
|
-
const
|
|
4215
|
+
const ownersByHost = /* @__PURE__ */ new Map();
|
|
2871
4216
|
for (const proj of Object.values(manifest.projections)) {
|
|
2872
|
-
|
|
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);
|
|
2873
4221
|
}
|
|
2874
|
-
const
|
|
2875
|
-
|
|
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) {
|
|
2876
4228
|
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
2877
|
-
const
|
|
2878
|
-
if (!
|
|
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;
|
|
2879
4235
|
if (!proj.managedMarker) continue;
|
|
2880
|
-
const content = await
|
|
4236
|
+
const content = await fs10.readFile(absPath, "utf8");
|
|
2881
4237
|
if (!HostProjector.hasManagedMarker(content)) continue;
|
|
2882
4238
|
if (proj.hash) {
|
|
2883
|
-
const diskHash = `sha256:${crypto4.createHash("sha256").update(await
|
|
4239
|
+
const diskHash = `sha256:${crypto4.createHash("sha256").update(await fs10.readFile(absPath)).digest("hex")}`;
|
|
2884
4240
|
if (diskHash !== proj.hash) {
|
|
2885
4241
|
const owner = proj.owners[0];
|
|
2886
|
-
|
|
4242
|
+
pushProjectionWarning(
|
|
4243
|
+
projRelPath,
|
|
2887
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.` : "")
|
|
2888
4245
|
);
|
|
2889
4246
|
continue;
|
|
@@ -2894,7 +4251,8 @@ var DoctorEngine = class {
|
|
|
2894
4251
|
const onDisk = content.replace(/\r\n/g, "\n");
|
|
2895
4252
|
if (!variants.some((variant) => variant.replace(/\r\n/g, "\n") === onDisk)) {
|
|
2896
4253
|
const owner = proj.owners[0];
|
|
2897
|
-
|
|
4254
|
+
pushProjectionWarning(
|
|
4255
|
+
projRelPath,
|
|
2898
4256
|
`Outdated projection ${projRelPath} (content differs from the current render).` + (owner ? ` Run: agents update ${owner} --fanout ${proj.host} to refresh it.` : "")
|
|
2899
4257
|
);
|
|
2900
4258
|
}
|
|
@@ -2902,7 +4260,7 @@ var DoctorEngine = class {
|
|
|
2902
4260
|
}
|
|
2903
4261
|
}
|
|
2904
4262
|
const installedBundles = manifest.installed?.bundles ?? [];
|
|
2905
|
-
const fileOwnersOf = (rec) =>
|
|
4263
|
+
const fileOwnersOf = (rec) => assetOwners(rec);
|
|
2906
4264
|
if (manifest.projections) {
|
|
2907
4265
|
for (const [projRelPath, proj] of Object.entries(manifest.projections)) {
|
|
2908
4266
|
for (const owner of proj.owners) {
|
|
@@ -2932,6 +4290,12 @@ var DoctorEngine = class {
|
|
|
2932
4290
|
if (!clineCapability.installed) {
|
|
2933
4291
|
warnings.push("Cline executable was not detected on PATH or via CLINE_BIN_PATH.");
|
|
2934
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
|
+
}
|
|
2935
4299
|
}
|
|
2936
4300
|
return {
|
|
2937
4301
|
valid: issues.length === 0,
|
|
@@ -2942,16 +4306,17 @@ var DoctorEngine = class {
|
|
|
2942
4306
|
agentsCount,
|
|
2943
4307
|
skillsCount,
|
|
2944
4308
|
workflowsCount,
|
|
2945
|
-
clineCapability
|
|
4309
|
+
clineCapability,
|
|
4310
|
+
claudeCapability
|
|
2946
4311
|
};
|
|
2947
4312
|
}
|
|
2948
4313
|
};
|
|
2949
4314
|
|
|
2950
4315
|
// src/core/cline-launcher.ts
|
|
2951
|
-
import
|
|
4316
|
+
import path13 from "path";
|
|
2952
4317
|
import os3 from "os";
|
|
2953
4318
|
import crypto5 from "crypto";
|
|
2954
|
-
import
|
|
4319
|
+
import fs11 from "fs-extra";
|
|
2955
4320
|
var ClineLauncher = class _ClineLauncher {
|
|
2956
4321
|
probe;
|
|
2957
4322
|
runner;
|
|
@@ -2964,7 +4329,7 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2964
4329
|
*/
|
|
2965
4330
|
static generateTeamName(bundleName, workspace) {
|
|
2966
4331
|
const sanitizedBundle = bundleName.replace(/[^A-Za-z0-9_-]/g, "-");
|
|
2967
|
-
const hash = crypto5.createHash("sha256").update(
|
|
4332
|
+
const hash = crypto5.createHash("sha256").update(path13.resolve(workspace)).digest("hex").slice(0, 8);
|
|
2968
4333
|
const prefix = `au-${sanitizedBundle}`;
|
|
2969
4334
|
const maxPrefixLen = 64 - 9;
|
|
2970
4335
|
const truncatedPrefix = prefix.slice(0, maxPrefixLen);
|
|
@@ -2991,16 +4356,16 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
2991
4356
|
workspace = os3.homedir();
|
|
2992
4357
|
} else {
|
|
2993
4358
|
workspace = cwd;
|
|
2994
|
-
targetDir =
|
|
4359
|
+
targetDir = path13.join(cwd, ".agents");
|
|
2995
4360
|
scope = "project";
|
|
2996
4361
|
}
|
|
2997
|
-
const lockfilePath =
|
|
2998
|
-
if (!await
|
|
4362
|
+
const lockfilePath = path13.join(targetDir, "agents-united.json");
|
|
4363
|
+
if (!await fs11.pathExists(lockfilePath)) {
|
|
2999
4364
|
if (!isGlobal) {
|
|
3000
4365
|
const globalTarget = AgentHostAdapter.resolveHostDir("global", "agents");
|
|
3001
|
-
const globalLockfile =
|
|
3002
|
-
if (await
|
|
3003
|
-
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);
|
|
3004
4369
|
if (gLock.installed?.bundles?.includes(bundleName)) {
|
|
3005
4370
|
return this.resolveInstallation(bundleName, { ...options, global: true });
|
|
3006
4371
|
}
|
|
@@ -3008,14 +4373,14 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
3008
4373
|
}
|
|
3009
4374
|
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
3010
4375
|
}
|
|
3011
|
-
const lockfile = await
|
|
4376
|
+
const lockfile = await fs11.readJson(lockfilePath);
|
|
3012
4377
|
if (!lockfile.installed?.bundles?.includes(bundleName)) {
|
|
3013
4378
|
throw new Error(`Bundle '${bundleName}' is not installed in ${scope} scope.`);
|
|
3014
4379
|
}
|
|
3015
4380
|
const hasClineFanout = (lockfile.fanout || []).includes("cline");
|
|
3016
4381
|
const manifestRel = `.agents/plugins/${bundleName}/agents-united/teams/${bundleName}.yaml`;
|
|
3017
|
-
const manifestPath =
|
|
3018
|
-
if (!hasClineFanout || !await
|
|
4382
|
+
const manifestPath = path13.join(workspace, manifestRel);
|
|
4383
|
+
if (!hasClineFanout || !await fs11.pathExists(manifestPath)) {
|
|
3019
4384
|
throw new Error(
|
|
3020
4385
|
`Bundle '${bundleName}' is not projected to Cline. Run 'agents update ${bundleName} --fanout cline' first.`
|
|
3021
4386
|
);
|
|
@@ -3114,15 +4479,190 @@ var ClineLauncher = class _ClineLauncher {
|
|
|
3114
4479
|
}
|
|
3115
4480
|
};
|
|
3116
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
|
+
|
|
3117
4657
|
// src/core/prerequisites.ts
|
|
3118
|
-
import
|
|
3119
|
-
import
|
|
3120
|
-
import
|
|
4658
|
+
import path16 from "path";
|
|
4659
|
+
import os6 from "os";
|
|
4660
|
+
import fs14 from "fs-extra";
|
|
3121
4661
|
|
|
3122
4662
|
// src/core/mcp-locations.ts
|
|
3123
|
-
import
|
|
3124
|
-
import
|
|
3125
|
-
import
|
|
4663
|
+
import path15 from "path";
|
|
4664
|
+
import os5 from "os";
|
|
4665
|
+
import fs13 from "fs-extra";
|
|
3126
4666
|
var McpLocationRegistry = class _McpLocationRegistry {
|
|
3127
4667
|
/**
|
|
3128
4668
|
* Declarative catalog of known MCP configuration files across AI tools, CLIs, extensions, and workspaces.
|
|
@@ -3134,7 +4674,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3134
4674
|
host: "gemini",
|
|
3135
4675
|
category: "cli",
|
|
3136
4676
|
label: "Google Antigravity & Gemini Global Config",
|
|
3137
|
-
resolvePath: (_, home) =>
|
|
4677
|
+
resolvePath: (_, home) => path15.join(home, ".gemini", "config", "mcp_config.json"),
|
|
3138
4678
|
isPrimary: true
|
|
3139
4679
|
},
|
|
3140
4680
|
{
|
|
@@ -3142,21 +4682,21 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3142
4682
|
host: "gemini",
|
|
3143
4683
|
category: "desktop",
|
|
3144
4684
|
label: "Google Antigravity System Config",
|
|
3145
|
-
resolvePath: (_, home) =>
|
|
4685
|
+
resolvePath: (_, home) => path15.join(home, ".gemini", "antigravity", "mcp_config.json")
|
|
3146
4686
|
},
|
|
3147
4687
|
{
|
|
3148
4688
|
id: "antigravity-workspace",
|
|
3149
4689
|
host: "gemini",
|
|
3150
4690
|
category: "workspace",
|
|
3151
4691
|
label: "Google Antigravity Local Workspace Config",
|
|
3152
|
-
resolvePath: (cwd) =>
|
|
4692
|
+
resolvePath: (cwd) => path15.join(cwd, ".gemini", "config", "mcp_config.json")
|
|
3153
4693
|
},
|
|
3154
4694
|
{
|
|
3155
4695
|
id: "gemini-appdata",
|
|
3156
4696
|
host: "gemini",
|
|
3157
4697
|
category: "cli",
|
|
3158
4698
|
label: "Google Gemini AppData Config",
|
|
3159
|
-
resolvePath: (_, __, appData) =>
|
|
4699
|
+
resolvePath: (_, __, appData) => path15.join(appData, "gemini", "config", "mcp_config.json")
|
|
3160
4700
|
},
|
|
3161
4701
|
// --- Cline CLI & VS Code Extension ---
|
|
3162
4702
|
{
|
|
@@ -3164,7 +4704,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3164
4704
|
host: "cline",
|
|
3165
4705
|
category: "cli",
|
|
3166
4706
|
label: "Cline CLI MCP Settings",
|
|
3167
|
-
resolvePath: (_, home) =>
|
|
4707
|
+
resolvePath: (_, home) => path15.join(home, ".cline", "data", "settings", "cline_mcp_settings.json"),
|
|
3168
4708
|
isPrimary: true
|
|
3169
4709
|
},
|
|
3170
4710
|
{
|
|
@@ -3172,42 +4712,42 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3172
4712
|
host: "cline",
|
|
3173
4713
|
category: "extension",
|
|
3174
4714
|
label: "Cline VS Code Extension Global Settings",
|
|
3175
|
-
resolvePath: (_, __, appData) =>
|
|
4715
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
3176
4716
|
},
|
|
3177
4717
|
{
|
|
3178
4718
|
id: "cline-insiders-extension-settings",
|
|
3179
4719
|
host: "cline",
|
|
3180
4720
|
category: "extension",
|
|
3181
4721
|
label: "Cline VS Code Insiders Extension Settings",
|
|
3182
|
-
resolvePath: (_, __, appData) =>
|
|
4722
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code - Insiders", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
3183
4723
|
},
|
|
3184
4724
|
{
|
|
3185
4725
|
id: "cline-legacy-settings",
|
|
3186
4726
|
host: "cline",
|
|
3187
4727
|
category: "cli",
|
|
3188
4728
|
label: "Cline Home Legacy Settings",
|
|
3189
|
-
resolvePath: (_, home) =>
|
|
4729
|
+
resolvePath: (_, home) => path15.join(home, ".cline", "settings", "cline_mcp_settings.json")
|
|
3190
4730
|
},
|
|
3191
4731
|
{
|
|
3192
4732
|
id: "cline-home-flat",
|
|
3193
4733
|
host: "cline",
|
|
3194
4734
|
category: "cli",
|
|
3195
4735
|
label: "Cline Home Flat Settings",
|
|
3196
|
-
resolvePath: (_, home) =>
|
|
4736
|
+
resolvePath: (_, home) => path15.join(home, "cline_mcp_settings.json")
|
|
3197
4737
|
},
|
|
3198
4738
|
{
|
|
3199
4739
|
id: "cline-workspace-flat",
|
|
3200
4740
|
host: "cline",
|
|
3201
4741
|
category: "workspace",
|
|
3202
4742
|
label: "Cline Workspace Settings",
|
|
3203
|
-
resolvePath: (cwd) =>
|
|
4743
|
+
resolvePath: (cwd) => path15.join(cwd, "cline_mcp_settings.json")
|
|
3204
4744
|
},
|
|
3205
4745
|
{
|
|
3206
4746
|
id: "cline-workspace-dot",
|
|
3207
4747
|
host: "cline",
|
|
3208
4748
|
category: "workspace",
|
|
3209
4749
|
label: "Cline Workspace .cline/mcp.json",
|
|
3210
|
-
resolvePath: (cwd) =>
|
|
4750
|
+
resolvePath: (cwd) => path15.join(cwd, ".cline", "mcp.json")
|
|
3211
4751
|
},
|
|
3212
4752
|
// --- Roo Code Extension ---
|
|
3213
4753
|
{
|
|
@@ -3215,7 +4755,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3215
4755
|
host: "cline",
|
|
3216
4756
|
category: "extension",
|
|
3217
4757
|
label: "Roo Code VS Code Extension Settings",
|
|
3218
|
-
resolvePath: (_, __, appData) =>
|
|
4758
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json")
|
|
3219
4759
|
},
|
|
3220
4760
|
// --- Cursor IDE / CLI ---
|
|
3221
4761
|
{
|
|
@@ -3223,7 +4763,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3223
4763
|
host: "cursor",
|
|
3224
4764
|
category: "workspace",
|
|
3225
4765
|
label: "Cursor Workspace Config",
|
|
3226
|
-
resolvePath: (cwd) =>
|
|
4766
|
+
resolvePath: (cwd) => path15.join(cwd, ".cursor", "mcp.json"),
|
|
3227
4767
|
isPrimary: true
|
|
3228
4768
|
},
|
|
3229
4769
|
{
|
|
@@ -3231,7 +4771,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3231
4771
|
host: "cursor",
|
|
3232
4772
|
category: "desktop",
|
|
3233
4773
|
label: "Cursor Global User Config",
|
|
3234
|
-
resolvePath: (_, home) =>
|
|
4774
|
+
resolvePath: (_, home) => path15.join(home, ".cursor", "mcp.json")
|
|
3235
4775
|
},
|
|
3236
4776
|
// --- Claude Code CLI & Claude Desktop ---
|
|
3237
4777
|
{
|
|
@@ -3239,7 +4779,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3239
4779
|
host: "claude",
|
|
3240
4780
|
category: "workspace",
|
|
3241
4781
|
label: "Claude Code Workspace Config",
|
|
3242
|
-
resolvePath: (cwd) =>
|
|
4782
|
+
resolvePath: (cwd) => path15.join(cwd, ".claude", "mcp.json"),
|
|
3243
4783
|
isPrimary: true
|
|
3244
4784
|
},
|
|
3245
4785
|
{
|
|
@@ -3247,28 +4787,28 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3247
4787
|
host: "claude",
|
|
3248
4788
|
category: "workspace",
|
|
3249
4789
|
label: "Claude Workspace claude.json",
|
|
3250
|
-
resolvePath: (cwd) =>
|
|
4790
|
+
resolvePath: (cwd) => path15.join(cwd, "claude.json")
|
|
3251
4791
|
},
|
|
3252
4792
|
{
|
|
3253
4793
|
id: "claude-global-dot",
|
|
3254
4794
|
host: "claude",
|
|
3255
4795
|
category: "cli",
|
|
3256
4796
|
label: "Claude Code Global Config",
|
|
3257
|
-
resolvePath: (_, home) =>
|
|
4797
|
+
resolvePath: (_, home) => path15.join(home, ".claude", "mcp.json")
|
|
3258
4798
|
},
|
|
3259
4799
|
{
|
|
3260
4800
|
id: "claude-global-flat",
|
|
3261
4801
|
host: "claude",
|
|
3262
4802
|
category: "cli",
|
|
3263
4803
|
label: "Claude Code Global claude.json",
|
|
3264
|
-
resolvePath: (_, home) =>
|
|
4804
|
+
resolvePath: (_, home) => path15.join(home, "claude.json")
|
|
3265
4805
|
},
|
|
3266
4806
|
{
|
|
3267
4807
|
id: "claude-desktop-global",
|
|
3268
4808
|
host: "claude",
|
|
3269
4809
|
category: "desktop",
|
|
3270
4810
|
label: "Claude Desktop Global Config",
|
|
3271
|
-
resolvePath: (_, __, appData) =>
|
|
4811
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Claude", "claude_desktop_config.json")
|
|
3272
4812
|
},
|
|
3273
4813
|
// --- Windsurf IDE ---
|
|
3274
4814
|
{
|
|
@@ -3276,7 +4816,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3276
4816
|
host: "windsurf",
|
|
3277
4817
|
category: "desktop",
|
|
3278
4818
|
label: "Windsurf Global Config",
|
|
3279
|
-
resolvePath: (_, home) =>
|
|
4819
|
+
resolvePath: (_, home) => path15.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
3280
4820
|
isPrimary: true
|
|
3281
4821
|
},
|
|
3282
4822
|
{
|
|
@@ -3284,7 +4824,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3284
4824
|
host: "windsurf",
|
|
3285
4825
|
category: "desktop",
|
|
3286
4826
|
label: "Windsurf AppData Config",
|
|
3287
|
-
resolvePath: (_, __, appData) =>
|
|
4827
|
+
resolvePath: (_, __, appData) => path15.join(appData, "Windsurf", "mcp_config.json")
|
|
3288
4828
|
},
|
|
3289
4829
|
// --- OpenCode & Codex ---
|
|
3290
4830
|
{
|
|
@@ -3292,7 +4832,7 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3292
4832
|
host: "opencode",
|
|
3293
4833
|
category: "workspace",
|
|
3294
4834
|
label: "OpenCode Workspace Config",
|
|
3295
|
-
resolvePath: (cwd) =>
|
|
4835
|
+
resolvePath: (cwd) => path15.join(cwd, ".opencode", "mcp.json"),
|
|
3296
4836
|
isPrimary: true
|
|
3297
4837
|
},
|
|
3298
4838
|
{
|
|
@@ -3300,28 +4840,28 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3300
4840
|
host: "opencode",
|
|
3301
4841
|
category: "cli",
|
|
3302
4842
|
label: "OpenCode Global Config",
|
|
3303
|
-
resolvePath: (_, home) =>
|
|
4843
|
+
resolvePath: (_, home) => path15.join(home, ".config", "opencode", "mcp.json")
|
|
3304
4844
|
}
|
|
3305
4845
|
];
|
|
3306
4846
|
/**
|
|
3307
4847
|
* Discovers all MCP configuration files across the filesystem, inspecting existence and parsing server definitions.
|
|
3308
4848
|
*/
|
|
3309
4849
|
static async discoverAll(cwd = process.cwd()) {
|
|
3310
|
-
const homeDir =
|
|
3311
|
-
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"));
|
|
3312
4852
|
const results = [];
|
|
3313
4853
|
const seenPaths = /* @__PURE__ */ new Set();
|
|
3314
4854
|
for (const desc of _McpLocationRegistry.LOCATIONS) {
|
|
3315
|
-
const resolved =
|
|
4855
|
+
const resolved = path15.normalize(desc.resolvePath(cwd, homeDir, appData));
|
|
3316
4856
|
const key = resolved.toLowerCase();
|
|
3317
4857
|
if (seenPaths.has(key)) continue;
|
|
3318
4858
|
seenPaths.add(key);
|
|
3319
|
-
const exists = await
|
|
4859
|
+
const exists = await fs13.pathExists(resolved);
|
|
3320
4860
|
let serverCount = 0;
|
|
3321
4861
|
let servers;
|
|
3322
4862
|
if (exists) {
|
|
3323
4863
|
try {
|
|
3324
|
-
const config = await
|
|
4864
|
+
const config = await fs13.readJson(resolved);
|
|
3325
4865
|
const rawServers = config?.mcpServers || config?.servers || config;
|
|
3326
4866
|
if (rawServers && typeof rawServers === "object") {
|
|
3327
4867
|
servers = rawServers;
|
|
@@ -3356,14 +4896,14 @@ var McpLocationRegistry = class _McpLocationRegistry {
|
|
|
3356
4896
|
* Resolves the primary target write path for a given host runtime.
|
|
3357
4897
|
*/
|
|
3358
4898
|
static getPrimaryWritePath(host, cwd = process.cwd()) {
|
|
3359
|
-
const homeDir =
|
|
3360
|
-
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"));
|
|
3361
4901
|
const targetHost = host === "agents" ? "gemini" : host;
|
|
3362
4902
|
const desc = _McpLocationRegistry.LOCATIONS.find((l) => l.host === targetHost && l.isPrimary);
|
|
3363
4903
|
if (desc) {
|
|
3364
|
-
return
|
|
4904
|
+
return path15.normalize(desc.resolvePath(cwd, homeDir, appData));
|
|
3365
4905
|
}
|
|
3366
|
-
return
|
|
4906
|
+
return path15.normalize(path15.join(cwd, ".cursor", "mcp.json"));
|
|
3367
4907
|
}
|
|
3368
4908
|
};
|
|
3369
4909
|
|
|
@@ -3478,21 +5018,21 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3478
5018
|
});
|
|
3479
5019
|
}
|
|
3480
5020
|
}
|
|
3481
|
-
const homeDir =
|
|
5021
|
+
const homeDir = os6.homedir();
|
|
3482
5022
|
const dirCandidates = [
|
|
3483
|
-
|
|
3484
|
-
|
|
5023
|
+
path16.join(cwd, ".gemini", "antigravity", "mcp"),
|
|
5024
|
+
path16.join(homeDir, ".gemini", "antigravity", "mcp")
|
|
3485
5025
|
];
|
|
3486
5026
|
for (const dir of dirCandidates) {
|
|
3487
|
-
if (await
|
|
3488
|
-
const entries = await
|
|
5027
|
+
if (await fs14.pathExists(dir)) {
|
|
5028
|
+
const entries = await fs14.readdir(dir).catch(() => []);
|
|
3489
5029
|
for (const entry of entries) {
|
|
3490
5030
|
if (_PrerequisiteChecker.matchesMcpServer(mcpName, entry, {})) {
|
|
3491
5031
|
foundInConfigs.push({
|
|
3492
5032
|
id: "antigravity-dir",
|
|
3493
5033
|
host: "gemini",
|
|
3494
5034
|
label: "Google Antigravity MCP Directory",
|
|
3495
|
-
path:
|
|
5035
|
+
path: path16.join(dir, entry),
|
|
3496
5036
|
disabled: false,
|
|
3497
5037
|
serverKey: entry
|
|
3498
5038
|
});
|
|
@@ -3517,14 +5057,14 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3517
5057
|
* Checks if an npm package is installed in node_modules or declared in package.json
|
|
3518
5058
|
*/
|
|
3519
5059
|
static async isPackageInstalled(pkgName, cwd = process.cwd()) {
|
|
3520
|
-
const modulePath =
|
|
3521
|
-
if (await
|
|
5060
|
+
const modulePath = path16.join(cwd, "node_modules", ...pkgName.split("/"));
|
|
5061
|
+
if (await fs14.pathExists(modulePath)) {
|
|
3522
5062
|
return { installed: true, location: modulePath };
|
|
3523
5063
|
}
|
|
3524
|
-
const pkgJsonPath =
|
|
3525
|
-
if (await
|
|
5064
|
+
const pkgJsonPath = path16.join(cwd, "package.json");
|
|
5065
|
+
if (await fs14.pathExists(pkgJsonPath)) {
|
|
3526
5066
|
try {
|
|
3527
|
-
const pkg = await
|
|
5067
|
+
const pkg = await fs14.readJson(pkgJsonPath);
|
|
3528
5068
|
const deps = {
|
|
3529
5069
|
...pkg.dependencies,
|
|
3530
5070
|
...pkg.devDependencies,
|
|
@@ -3547,20 +5087,20 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3547
5087
|
return { set: true, source: "process.env" };
|
|
3548
5088
|
}
|
|
3549
5089
|
const envFiles = [
|
|
3550
|
-
|
|
3551
|
-
|
|
5090
|
+
path16.join(cwd, ".env.local"),
|
|
5091
|
+
path16.join(cwd, ".env")
|
|
3552
5092
|
];
|
|
3553
5093
|
for (const envFile of envFiles) {
|
|
3554
|
-
if (await
|
|
5094
|
+
if (await fs14.pathExists(envFile)) {
|
|
3555
5095
|
try {
|
|
3556
|
-
const content = await
|
|
5096
|
+
const content = await fs14.readFile(envFile, "utf8");
|
|
3557
5097
|
const lines = content.split("\n");
|
|
3558
5098
|
for (const line of lines) {
|
|
3559
5099
|
const trimmed = line.trim();
|
|
3560
5100
|
if (trimmed.startsWith("#") || !trimmed.includes("=")) continue;
|
|
3561
5101
|
const [k, ...v] = trimmed.split("=");
|
|
3562
5102
|
if (k.trim() === varName && v.join("=").trim() !== "") {
|
|
3563
|
-
return { set: true, source:
|
|
5103
|
+
return { set: true, source: path16.basename(envFile) };
|
|
3564
5104
|
}
|
|
3565
5105
|
}
|
|
3566
5106
|
} catch {
|
|
@@ -3766,11 +5306,11 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3766
5306
|
* activating disabled servers and appending missing definitions.
|
|
3767
5307
|
*/
|
|
3768
5308
|
static async autoConfigureConfigFile(targetFilePath, mcpNames, mode = "operational", envVars = {}) {
|
|
3769
|
-
await
|
|
5309
|
+
await fs14.ensureDir(path16.dirname(targetFilePath));
|
|
3770
5310
|
let existingConfig = {};
|
|
3771
|
-
if (await
|
|
5311
|
+
if (await fs14.pathExists(targetFilePath)) {
|
|
3772
5312
|
try {
|
|
3773
|
-
existingConfig = await
|
|
5313
|
+
existingConfig = await fs14.readJson(targetFilePath);
|
|
3774
5314
|
} catch {
|
|
3775
5315
|
existingConfig = {};
|
|
3776
5316
|
}
|
|
@@ -3802,7 +5342,7 @@ var PrerequisiteChecker = class _PrerequisiteChecker {
|
|
|
3802
5342
|
} else {
|
|
3803
5343
|
existingConfig.mcpServers = existingServers;
|
|
3804
5344
|
}
|
|
3805
|
-
await
|
|
5345
|
+
await fs14.writeJson(targetFilePath, existingConfig, { spaces: 2 });
|
|
3806
5346
|
return {
|
|
3807
5347
|
success: true,
|
|
3808
5348
|
targetFile: targetFilePath,
|
|
@@ -3847,7 +5387,7 @@ var updater = new UpdateEngine(registry, scanner, installer);
|
|
|
3847
5387
|
function detectWorkspaceHosts(cwd = process.cwd()) {
|
|
3848
5388
|
const detected = [];
|
|
3849
5389
|
for (const host of Object.values(HOST_REGISTRY)) {
|
|
3850
|
-
const found = host.detectionMarkers.some((marker) =>
|
|
5390
|
+
const found = host.detectionMarkers.some((marker) => fs15.pathExistsSync(path17.join(cwd, marker)));
|
|
3851
5391
|
if (found) detected.push(host.id);
|
|
3852
5392
|
}
|
|
3853
5393
|
return detected;
|
|
@@ -4078,7 +5618,7 @@ var BUNDLE_DISPLAY_NAMES = {
|
|
|
4078
5618
|
summary: "Complete suite with all 7 team leads, 38 agents, and all 160 modular skills & workflows"
|
|
4079
5619
|
}
|
|
4080
5620
|
};
|
|
4081
|
-
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 = {}) => {
|
|
4082
5622
|
intro(pc.cyan("Agents United \u2014 AI Agent Ecosystem"));
|
|
4083
5623
|
let identifier = targetIdentifier;
|
|
4084
5624
|
let scope = options.global ? "global" : "project";
|
|
@@ -4112,6 +5652,14 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
4112
5652
|
);
|
|
4113
5653
|
}
|
|
4114
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
|
+
}
|
|
4115
5663
|
}
|
|
4116
5664
|
const isInteractive = process.stdout.isTTY && !options.yes;
|
|
4117
5665
|
if (isInteractive && !options.global && !options.copy && !options.symlink && options.target === "agents") {
|
|
@@ -4122,6 +5670,7 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
4122
5670
|
"Workspace Environment Discovery"
|
|
4123
5671
|
);
|
|
4124
5672
|
}
|
|
5673
|
+
note(hostAvailabilityNotice(), "Host availability");
|
|
4125
5674
|
const hostSelection = await multiselect({
|
|
4126
5675
|
message: "1. Which AI tools should we configure agent teams & personas for?",
|
|
4127
5676
|
options: [
|
|
@@ -4135,33 +5684,18 @@ cli.command("add [identifier]", "Add a bundle, agent, skill, or workflow to proj
|
|
|
4135
5684
|
label: HOST_REGISTRY.claude.label,
|
|
4136
5685
|
hint: detectedHosts.includes("claude") ? "found in this project" : "orchestrator & subagent personas for Claude Code"
|
|
4137
5686
|
},
|
|
4138
|
-
{
|
|
4139
|
-
value: "cursor",
|
|
4140
|
-
label: HOST_REGISTRY.cursor.label,
|
|
4141
|
-
hint: detectedHosts.includes("cursor") ? "found in this project" : "orchestrator & subagent personas for Cursor IDE"
|
|
4142
|
-
},
|
|
4143
5687
|
{
|
|
4144
5688
|
value: "cline",
|
|
4145
5689
|
label: HOST_REGISTRY.cline.label,
|
|
4146
5690
|
hint: detectedHosts.includes("cline") ? "found in this project" : "configured agents, skills, rules, workflows & team manifests for Cline"
|
|
4147
5691
|
},
|
|
4148
|
-
{
|
|
4149
|
-
value: "opencode",
|
|
4150
|
-
label: HOST_REGISTRY.opencode.label,
|
|
4151
|
-
hint: detectedHosts.includes("opencode") ? "found in this project" : "orchestrator & subagent personas for OpenCode"
|
|
4152
|
-
},
|
|
4153
|
-
{
|
|
4154
|
-
value: "codex",
|
|
4155
|
-
label: HOST_REGISTRY.codex.label,
|
|
4156
|
-
hint: detectedHosts.includes("codex") ? "found in this project" : "root index linking orchestrators, subagents & skills for Codex, Copilot, Aider & Zed"
|
|
4157
|
-
},
|
|
4158
5692
|
{
|
|
4159
5693
|
value: "gemini",
|
|
4160
5694
|
label: HOST_REGISTRY.gemini.label,
|
|
4161
5695
|
hint: detectedHosts.includes("gemini") ? "found in this project" : "older Antigravity 1.0 / Gemini folder"
|
|
4162
5696
|
}
|
|
4163
5697
|
],
|
|
4164
|
-
initialValues: detectedHosts.length > 0 ? detectedHosts : ["agents"],
|
|
5698
|
+
initialValues: detectedHosts.length > 0 ? detectedHosts.filter((h) => HOST_REGISTRY[h]?.status === "supported") : ["agents"],
|
|
4165
5699
|
required: true
|
|
4166
5700
|
});
|
|
4167
5701
|
if (Array.isArray(hostSelection) && hostSelection.length > 0) {
|
|
@@ -4466,6 +6000,10 @@ To bypass this gate and install draft assets anyway, pass: --allow-under-constru
|
|
|
4466
6000
|
method,
|
|
4467
6001
|
hosts,
|
|
4468
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,
|
|
4469
6007
|
mode: executionMode,
|
|
4470
6008
|
allowMissingPrereqs: options.allowMissingPrereqs,
|
|
4471
6009
|
yes: options.yes,
|
|
@@ -4589,6 +6127,14 @@ rules & workflows run: agents update ${identifier} --fanout cline
|
|
|
4589
6127
|
"Adaptive Tooling"
|
|
4590
6128
|
);
|
|
4591
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
|
+
}
|
|
4592
6138
|
outro(pc.green(`\u2714 Installed "${identifier}" successfully!`));
|
|
4593
6139
|
} catch (err) {
|
|
4594
6140
|
s.stop(pc.red("Failed resolution"));
|
|
@@ -4656,10 +6202,25 @@ cli.command("remove [identifier]", "Remove an installed bundle, agent, skill, or
|
|
|
4656
6202
|
});
|
|
4657
6203
|
s.stop(`Uninstall processed`);
|
|
4658
6204
|
if (options.dryRun) {
|
|
4659
|
-
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
|
+
));
|
|
4660
6208
|
return;
|
|
4661
6209
|
}
|
|
4662
|
-
|
|
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
|
+
));
|
|
4663
6224
|
} catch (err) {
|
|
4664
6225
|
s.stop(pc.red("Uninstall failed"));
|
|
4665
6226
|
outro(pc.red(`Error: ${err.message}`));
|
|
@@ -4681,6 +6242,16 @@ cli.command("update [identifier]", "Update installed packages to upstream regist
|
|
|
4681
6242
|
"Invalid fanout"
|
|
4682
6243
|
);
|
|
4683
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
|
+
}
|
|
4684
6255
|
updateFanout = parsedFanout.filter((h) => isKnownHost(h) && HOST_REGISTRY[h].projectionCapable);
|
|
4685
6256
|
}
|
|
4686
6257
|
const s = spinner();
|
|
@@ -4803,7 +6374,7 @@ Updates Available: ${report.outdatedCount > 0 ? pc.yellow(pc.bold(`${report.outd
|
|
|
4803
6374
|
const unprojected = [];
|
|
4804
6375
|
for (const rec of result.updated) {
|
|
4805
6376
|
try {
|
|
4806
|
-
const lf = await
|
|
6377
|
+
const lf = await fs15.readJson(path17.join(rec.targetDir, "agents-united.json"));
|
|
4807
6378
|
if (!lf.fanout || lf.fanout.length === 0) unprojected.push(rec.name);
|
|
4808
6379
|
} catch {
|
|
4809
6380
|
}
|
|
@@ -5242,6 +6813,12 @@ Planned Capabilities:
|
|
|
5242
6813
|
method: "symlink"
|
|
5243
6814
|
});
|
|
5244
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
|
+
);
|
|
5245
6822
|
note(
|
|
5246
6823
|
formatInstallationSummary({
|
|
5247
6824
|
bundleName: result.installed.targetBundle || bundle.name,
|
|
@@ -5537,11 +7114,19 @@ cli.command("init", "Initialize project workspace directory and install recommen
|
|
|
5537
7114
|
process.exit(1);
|
|
5538
7115
|
}
|
|
5539
7116
|
});
|
|
5540
|
-
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 = {}) => {
|
|
5541
7118
|
intro(pc.cyan("Agents United \u2014 Runtime Activation"));
|
|
5542
7119
|
const launcher = new ClineLauncher();
|
|
5543
7120
|
const probe = new ClineCapabilityProbe();
|
|
5544
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
|
+
}
|
|
5545
7130
|
const resolution = await launcher.resolveInstallation(bundle, {
|
|
5546
7131
|
global: options.global,
|
|
5547
7132
|
cwd: process.cwd()
|
|
@@ -5552,6 +7137,7 @@ cli.command("start <bundle> [prompt]", "Start an installed bundle team in its ho
|
|
|
5552
7137
|
process.exit(1);
|
|
5553
7138
|
}
|
|
5554
7139
|
const bundleDef = await registry.getBundle(bundle);
|
|
7140
|
+
const organizationTier = bundleDef?.tier === "organization";
|
|
5555
7141
|
const plan = launcher.planActivation({
|
|
5556
7142
|
bundleName: bundle,
|
|
5557
7143
|
workspace: resolution.workspace,
|
|
@@ -5566,6 +7152,8 @@ cli.command("start <bundle> [prompt]", "Start an installed bundle team in its ho
|
|
|
5566
7152
|
if (options.dryRun) {
|
|
5567
7153
|
note(
|
|
5568
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"}
|
|
5569
7157
|
Scope: ${plan.scope}
|
|
5570
7158
|
Workspace: ${plan.workspace}
|
|
5571
7159
|
Team Name: ${plan.teamName}
|
|
@@ -5577,6 +7165,16 @@ Argv: ${plan.argv.map((a) => a.includes(" ") || a.includes("\n") ? `"${a.replace
|
|
|
5577
7165
|
outro(pc.yellow("Dry run complete. No processes launched."));
|
|
5578
7166
|
return;
|
|
5579
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
|
+
}
|
|
5580
7178
|
note(
|
|
5581
7179
|
`Team: ${pc.bold(plan.teamName)}
|
|
5582
7180
|
Strategy: ${pc.cyan(plan.strategy)}
|
|
@@ -5590,7 +7188,7 @@ Workspace: ${plan.workspace}`,
|
|
|
5590
7188
|
process.exit(1);
|
|
5591
7189
|
}
|
|
5592
7190
|
});
|
|
5593
|
-
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 = {}) => {
|
|
5594
7192
|
intro(pc.cyan("\u{1FA7A} Agents United \u2014 Health Doctor"));
|
|
5595
7193
|
const report = await DoctorEngine.runDoctor(void 0, options.host);
|
|
5596
7194
|
if (!report.isInitialized || report.agentsCount === 0 && report.skillsCount === 0 && report.workflowsCount === 0 && report.issues.length === 0) {
|
|
@@ -5612,6 +7210,13 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5612
7210
|
console.log(` Named Teams: ${report.clineCapability.namedTeams ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported (Adaptive fallback)")}
|
|
5613
7211
|
`);
|
|
5614
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
|
+
}
|
|
5615
7220
|
if (report.warnings.length > 0) {
|
|
5616
7221
|
console.log(pc.yellow(pc.bold("\u26A0\uFE0F Warnings:")));
|
|
5617
7222
|
report.warnings.forEach((w) => console.log(` \u26A0 ${w}`));
|
|
@@ -5632,6 +7237,11 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5632
7237
|
}
|
|
5633
7238
|
console.log(` Named Teams: ${report.clineCapability.namedTeams ? pc.green("\u2714 Supported") : pc.yellow("\u2716 Unsupported (Adaptive fallback)")}`);
|
|
5634
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)
|
|
5635
7245
|
`);
|
|
5636
7246
|
}
|
|
5637
7247
|
if (report.issues.length > 0) {
|
|
@@ -5651,15 +7261,176 @@ cli.command("doctor", "Verify health of installed agents, frontmatter schemas, a
|
|
|
5651
7261
|
process.exit(1);
|
|
5652
7262
|
}
|
|
5653
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
|
+
}
|
|
5654
7423
|
cli.help();
|
|
5655
7424
|
cli.version("1.0.0");
|
|
5656
7425
|
cli.parse();
|
|
5657
7426
|
export {
|
|
7427
|
+
describeClaudeTeamsPosture,
|
|
5658
7428
|
detectWorkspaceHosts,
|
|
5659
7429
|
formatInstallationSummary,
|
|
5660
7430
|
formatWrappedList,
|
|
5661
7431
|
getTerminalContentWidth,
|
|
5662
7432
|
renderProjections,
|
|
7433
|
+
resolveClaudeTeamsPosture,
|
|
5663
7434
|
wrapText
|
|
5664
7435
|
};
|
|
5665
7436
|
//# sourceMappingURL=cli.js.map
|