agentwheel 0.14.13 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,9 +11,9 @@ import {
11
11
  // src/cli/index.ts
12
12
  import { createHash as createHash11 } from "crypto";
13
13
  import { existsSync } from "fs";
14
- import { mkdir as mkdir22, rm as rm11, writeFile as writeFile21 } from "fs/promises";
14
+ import { mkdir as mkdir23, rm as rm11, writeFile as writeFile22 } from "fs/promises";
15
15
  import { homedir as homedir9 } from "os";
16
- import { dirname as dirname31, join as join41, resolve as resolve20 } from "path";
16
+ import { dirname as dirname32, join as join44, resolve as resolve22 } from "path";
17
17
  import { fileURLToPath as fileURLToPath3 } from "url";
18
18
  import { Command } from "commander";
19
19
 
@@ -810,13 +810,13 @@ async function spawnWithInput(command, args, input) {
810
810
  if (result.stderr) throw new Error(result.stderr);
811
811
  }
812
812
  function waitForProcess(child, label) {
813
- return new Promise((resolve21, reject) => {
813
+ return new Promise((resolve23, reject) => {
814
814
  const stderr = [];
815
815
  child.stderr?.on("data", (chunk) => stderr.push(chunk));
816
816
  child.on("error", reject);
817
817
  child.on("close", (code) => {
818
818
  const message = Buffer.concat(stderr).toString("utf8");
819
- if (code === 0) resolve21({ stderr: "" });
819
+ if (code === 0) resolve23({ stderr: "" });
820
820
  else reject(new Error(`${label} exited ${code}${message ? `: ${message}` : ""}`));
821
821
  });
822
822
  });
@@ -2592,6 +2592,7 @@ import { basename as basename8, join as join16, relative as relative3 } from "pa
2592
2592
  // src/staging/codex-subagents.ts
2593
2593
  import { mkdir as mkdir9, readFile as readFile11, writeFile as writeFile11 } from "fs/promises";
2594
2594
  import { basename as basename4, dirname as dirname12, join as join6 } from "path";
2595
+ import { parse as parseYaml } from "yaml";
2595
2596
  var requiredCodexAgentFields = ["name", "description", "developer_instructions"];
2596
2597
  async function renderCodexSubagents(artifacts, stageRoot, adapter) {
2597
2598
  if (adapter?.name !== "codex") return artifacts;
@@ -2664,24 +2665,36 @@ function markdownToCodexAgentToml(agentName, markdown) {
2664
2665
  const parsed = splitFrontmatter(markdown);
2665
2666
  const description = parsed.description ?? firstMeaningfulMarkdownLine(parsed.body) ?? `Custom Codex subagent ${agentName}.`;
2666
2667
  const developerInstructions = parsed.body.trim().length > 0 ? parsed.body.trimEnd() : description;
2667
- return [
2668
+ const lines = [
2668
2669
  `name = ${tomlString(agentName)}`,
2669
- `description = ${tomlString(description)}`,
2670
- `developer_instructions = ${tomlMultilineString(developerInstructions)}`,
2671
- ""
2672
- ].join("\n");
2670
+ `description = ${tomlString(description)}`
2671
+ ];
2672
+ if (parsed.model) lines.push(`model = ${tomlString(parsed.model)}`);
2673
+ if (parsed.modelReasoningEffort) lines.push(`model_reasoning_effort = ${tomlString(parsed.modelReasoningEffort)}`);
2674
+ lines.push(`developer_instructions = ${tomlMultilineString(developerInstructions)}`, "");
2675
+ return lines.join("\n");
2673
2676
  }
2674
2677
  function splitFrontmatter(markdown) {
2675
2678
  const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(markdown);
2676
2679
  if (!match) return { body: markdown };
2677
2680
  const frontmatter = match[1] ?? "";
2678
2681
  const body = markdown.slice(match[0].length);
2679
- const description = frontmatter.split(/\r?\n/).map((line) => /^description:\s*(?:"([^"]*)"|'([^']*)'|(.+))\s*$/.exec(line.trim())).find((item) => item !== null);
2682
+ const metadata = parseYaml(frontmatter);
2680
2683
  return {
2681
2684
  body,
2682
- description: description ? (description[1] ?? description[2] ?? description[3] ?? "").trim() : void 0
2685
+ description: optionalFrontmatterString(metadata, "description"),
2686
+ model: optionalFrontmatterString(metadata, "model"),
2687
+ modelReasoningEffort: optionalFrontmatterString(metadata, "model_reasoning_effort")
2683
2688
  };
2684
2689
  }
2690
+ function optionalFrontmatterString(metadata, key) {
2691
+ const value = metadata?.[key];
2692
+ if (value === void 0 || value === null || value === "") return void 0;
2693
+ if (typeof value !== "string") {
2694
+ throw new Error(`Codex subagent frontmatter field '${key}' must be a string.`);
2695
+ }
2696
+ return value.trim();
2697
+ }
2685
2698
  function firstMeaningfulMarkdownLine(markdown) {
2686
2699
  for (const rawLine of markdown.split(/\r?\n/)) {
2687
2700
  const line = rawLine.trim();
@@ -5250,14 +5263,14 @@ async function writeMermaidAsset(response) {
5250
5263
  "content-type": "application/javascript; charset=utf-8",
5251
5264
  "cache-control": "public, max-age=3600"
5252
5265
  });
5253
- await new Promise((resolve21) => {
5266
+ await new Promise((resolve23) => {
5254
5267
  const stream = createReadStream(assetPath);
5255
5268
  stream.on("error", () => {
5256
5269
  if (!response.headersSent) writeText(response, 404, "Mermaid asset unavailable.\n");
5257
5270
  else response.destroy();
5258
- resolve21();
5271
+ resolve23();
5259
5272
  });
5260
- stream.on("end", resolve21);
5273
+ stream.on("end", resolve23);
5261
5274
  stream.pipe(response);
5262
5275
  });
5263
5276
  }
@@ -5284,14 +5297,14 @@ function escapeHtml2(value) {
5284
5297
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
5285
5298
  }
5286
5299
  function listen(server, port, bind) {
5287
- return new Promise((resolve21, reject) => {
5300
+ return new Promise((resolve23, reject) => {
5288
5301
  const onError = (error) => {
5289
5302
  server.off("listening", onListening);
5290
5303
  reject(error);
5291
5304
  };
5292
5305
  const onListening = () => {
5293
5306
  server.off("error", onError);
5294
- resolve21();
5307
+ resolve23();
5295
5308
  };
5296
5309
  server.once("error", onError);
5297
5310
  server.once("listening", onListening);
@@ -5299,7 +5312,7 @@ function listen(server, port, bind) {
5299
5312
  });
5300
5313
  }
5301
5314
  function waitForShutdown(server, beforeClose) {
5302
- return new Promise((resolve21) => {
5315
+ return new Promise((resolve23) => {
5303
5316
  let closing = false;
5304
5317
  const shutdown = () => {
5305
5318
  if (closing) return;
@@ -5307,7 +5320,7 @@ function waitForShutdown(server, beforeClose) {
5307
5320
  process.off("SIGINT", shutdown);
5308
5321
  process.off("SIGTERM", shutdown);
5309
5322
  beforeClose();
5310
- server.close(() => resolve21());
5323
+ server.close(() => resolve23());
5311
5324
  };
5312
5325
  process.once("SIGINT", shutdown);
5313
5326
  process.once("SIGTERM", shutdown);
@@ -5317,7 +5330,7 @@ function waitForShutdown(server, beforeClose) {
5317
5330
  process.off("SIGINT", shutdown);
5318
5331
  process.off("SIGTERM", shutdown);
5319
5332
  beforeClose();
5320
- resolve21();
5333
+ resolve23();
5321
5334
  });
5322
5335
  });
5323
5336
  }
@@ -5837,7 +5850,7 @@ async function fetchClawHubPackage(fetchImpl, packageName) {
5837
5850
  throw new Error(`ClawHub lookup failed for ${packageName}`);
5838
5851
  }
5839
5852
  async function delay(ms) {
5840
- await new Promise((resolve21) => setTimeout(resolve21, ms));
5853
+ await new Promise((resolve23) => setTimeout(resolve23, ms));
5841
5854
  }
5842
5855
  function parseClawHubSource(source) {
5843
5856
  if (!source.startsWith(sourcePrefix)) {
@@ -5931,9 +5944,9 @@ var GitSourceDriver = class {
5931
5944
  throw new Error(`Frozen lock requires cached git checkout at ${resolved.resolvedPath}`);
5932
5945
  }
5933
5946
  await rm6(resolved.resolvedPath, { recursive: true, force: true });
5934
- await git(["clone", "--no-tags", parsed.url, resolved.resolvedPath]);
5947
+ await git(["clone", parsed.url, resolved.resolvedPath]);
5935
5948
  } else if (!resolved.frozenLock) {
5936
- await git(["-C", resolved.resolvedPath, "fetch", "--prune", "origin"]);
5949
+ await git(["-C", resolved.resolvedPath, "fetch", "--tags", "--prune", "origin"]);
5937
5950
  }
5938
5951
  const ref = resolved.requestedRef ?? parsed.ref ?? "HEAD";
5939
5952
  if (ref === "HEAD") {
@@ -6029,7 +6042,7 @@ async function withFilesystemLock(lockPath, timeoutMs, fn) {
6029
6042
  if (Date.now() - started > timeoutMs) {
6030
6043
  throw new Error(`Timed out waiting for git cache lock at ${lockPath}`);
6031
6044
  }
6032
- await new Promise((resolve21) => setTimeout(resolve21, 50));
6045
+ await new Promise((resolve23) => setTimeout(resolve23, 50));
6033
6046
  }
6034
6047
  }
6035
6048
  try {
@@ -6538,8 +6551,8 @@ function getSourceDriver(name = "local") {
6538
6551
  }
6539
6552
 
6540
6553
  // src/staging/staging.ts
6541
- import { chmod, cp as cp5, mkdir as mkdir17, mkdtemp as mkdtemp3, readdir as readdir5, stat as stat8 } from "fs/promises";
6542
- import { basename as basename18, dirname as dirname22, join as join28, relative as relative7, resolve as resolve12, sep as sep2 } from "path";
6554
+ import { chmod, cp as cp5, mkdir as mkdir18, mkdtemp as mkdtemp3, readdir as readdir5, stat as stat8 } from "fs/promises";
6555
+ import { basename as basename19, dirname as dirname23, join as join29, relative as relative7, resolve as resolve12, sep as sep2 } from "path";
6543
6556
  import { tmpdir as tmpdir4 } from "os";
6544
6557
 
6545
6558
  // src/compose/markdown.ts
@@ -6953,6 +6966,57 @@ async function sortedDirEntries2(path) {
6953
6966
  return (await readdir4(path, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
6954
6967
  }
6955
6968
 
6969
+ // src/staging/claude-subagents.ts
6970
+ import { mkdir as mkdir17, readFile as readFile21, writeFile as writeFile19 } from "fs/promises";
6971
+ import { basename as basename18, dirname as dirname22, join as join28 } from "path";
6972
+ async function renderClaudeSubagents(artifacts, stageRoot, adapter) {
6973
+ if (adapter?.name !== "claude") return artifacts;
6974
+ const names = /* @__PURE__ */ new Set();
6975
+ const rendered = [];
6976
+ for (const artifact of artifacts) {
6977
+ if (artifact.type !== "subagents") {
6978
+ rendered.push(artifact);
6979
+ continue;
6980
+ }
6981
+ const next = await renderClaudeSubagent(artifact, stageRoot);
6982
+ if (names.has(next.name)) {
6983
+ throw new Error(`Claude subagents produce duplicate agent name '${next.name}'.`);
6984
+ }
6985
+ names.add(next.name);
6986
+ rendered.push(next);
6987
+ }
6988
+ return rendered;
6989
+ }
6990
+ async function renderClaudeSubagent(artifact, stageRoot) {
6991
+ const sourcePath = artifact.stagedPath ?? artifact.sourcePath;
6992
+ const agentName = claudeAgentName(artifact);
6993
+ const markdownPath = artifact.kind === "dir" ? join28(sourcePath, "AGENTS.md") : sourcePath;
6994
+ if (artifact.kind === "dir" && !await pathExists(markdownPath)) {
6995
+ throw new Error(`Claude subagent directory ${artifact.relativePath} must contain AGENTS.md.`);
6996
+ }
6997
+ if (artifact.kind === "file" && !artifact.name.toLowerCase().endsWith(".md") && !sourcePath.toLowerCase().endsWith(".md")) {
6998
+ throw new Error(`Claude subagent ${artifact.relativePath} must be a .md file or directory containing AGENTS.md.`);
6999
+ }
7000
+ const content = await readFile21(markdownPath, "utf8");
7001
+ const renderedPath = join28(stageRoot, ".agentwheel-rendered", "claude-subagents", `${agentName}.md`);
7002
+ await mkdir17(dirname22(renderedPath), { recursive: true });
7003
+ await writeFile19(renderedPath, content.endsWith("\n") ? content : `${content}
7004
+ `, "utf8");
7005
+ return {
7006
+ ...artifact,
7007
+ name: `${agentName}.md`,
7008
+ sourcePath: renderedPath,
7009
+ stagedPath: renderedPath,
7010
+ relativePath: join28("subagents", `${agentName}.md`),
7011
+ kind: "file",
7012
+ hash: await hashPath(renderedPath)
7013
+ };
7014
+ }
7015
+ function claudeAgentName(artifact) {
7016
+ const raw = artifact.kind === "dir" ? artifact.name : basename18(artifact.name);
7017
+ return raw.replace(/\.agent\.md$/i, "").replace(/\.md$/i, "");
7018
+ }
7019
+
6956
7020
  // src/staging/staging.ts
6957
7021
  async function stageSource(driver, source, options = {}) {
6958
7022
  return renderStagedBundle(await stageSourceRaw(driver, source, options), options);
@@ -6966,15 +7030,15 @@ async function stageResolvedSourceRaw(driver, resolved) {
6966
7030
  return stageResolvedArtifactsRaw(resolved, artifacts);
6967
7031
  }
6968
7032
  async function stageResolvedArtifactsRaw(resolved, artifacts) {
6969
- const root = await mkdtemp3(join28(tmpdir4(), "agentwheel-stage-"));
7033
+ const root = await mkdtemp3(join29(tmpdir4(), "agentwheel-stage-"));
6970
7034
  const stagedArtifacts = [];
6971
7035
  for (const artifact of artifacts) {
6972
- const stagedPath = join28(root, artifact.relativePath);
6973
- await mkdir17(dirname22(stagedPath), { recursive: true });
7036
+ const stagedPath = join29(root, artifact.relativePath);
7037
+ await mkdir18(dirname23(stagedPath), { recursive: true });
6974
7038
  await cp5(artifact.sourcePath, stagedPath, {
6975
7039
  recursive: artifact.kind === "dir",
6976
7040
  dereference: true,
6977
- filter: (path) => !isIgnoredGeneratedEntry(basename18(path))
7041
+ filter: (path) => !isIgnoredGeneratedEntry(basename19(path))
6978
7042
  });
6979
7043
  await composeAssets(artifact, resolved.resolvedPath, stagedPath);
6980
7044
  stagedArtifacts.push({
@@ -7002,7 +7066,8 @@ async function renderStagedBundle(bundle, options = {}) {
7002
7066
  const selectedArtifacts = filterArtifactsBySelection(expandedArtifacts, options.select, options.skills);
7003
7067
  const runtimeSelectedSet = new Set(normalizeArtifactSelectors(options.select, options.skills) ?? []);
7004
7068
  const runtimeArtifacts = options.adapter ? filterArtifactsByRuntime(selectedArtifacts, options.adapter.name, runtimeSelectedSet) : selectedArtifacts;
7005
- const codexRenderedArtifacts = await renderCodexSubagents(runtimeArtifacts, root, options.adapter);
7069
+ const claudeRenderedArtifacts = await renderClaudeSubagents(runtimeArtifacts, root, options.adapter);
7070
+ const codexRenderedArtifacts = await renderCodexSubagents(claudeRenderedArtifacts, root, options.adapter);
7006
7071
  const openClawRenderedArtifacts = await renderOpenClawSubagents(codexRenderedArtifacts, root, options.adapter);
7007
7072
  const renderedArtifacts = await renderCopilotArtifacts(openClawRenderedArtifacts, root, options.adapter);
7008
7073
  const finalArtifacts = options.workspaceRoot && options.adapter ? await applyCustomizations(renderedArtifacts, {
@@ -7056,16 +7121,16 @@ async function composeAssets(artifact, packageRoot, stagedPath) {
7056
7121
  }
7057
7122
  for (const asset of artifact.assets) {
7058
7123
  const source = resolvePackagePath(packageRoot, asset.from);
7059
- const dest = join28(stagedPath, asset.into);
7124
+ const dest = join29(stagedPath, asset.into);
7060
7125
  await copyAsset(asset, source, dest);
7061
7126
  }
7062
7127
  }
7063
7128
  async function copyAsset(asset, source, dest) {
7064
7129
  const sourceStats = await stat8(source);
7065
7130
  if (sourceStats.isFile()) {
7066
- if (matchesAny(basename18(source), asset.include)) {
7067
- await mkdir17(dest, { recursive: true });
7068
- await copyAssetFile(source, join28(dest, basename18(source)), asset);
7131
+ if (matchesAny(basename19(source), asset.include)) {
7132
+ await mkdir18(dest, { recursive: true });
7133
+ await copyAssetFile(source, join29(dest, basename19(source)), asset);
7069
7134
  }
7070
7135
  return;
7071
7136
  }
@@ -7073,19 +7138,19 @@ async function copyAsset(asset, source, dest) {
7073
7138
  throw new Error(`Asset include source is not a file or directory: ${source}`);
7074
7139
  }
7075
7140
  if (!asset.include?.length) {
7076
- await mkdir17(dirname22(dest), { recursive: true });
7141
+ await mkdir18(dirname23(dest), { recursive: true });
7077
7142
  await cp5(source, dest, { recursive: true, dereference: true });
7078
7143
  if (asset.mode === "copy") await normalizeCopiedModes(dest);
7079
7144
  return;
7080
7145
  }
7081
7146
  for (const file of await listFiles(source)) {
7082
7147
  const rel = relative7(source, file).replaceAll("\\", "/");
7083
- if (!matchesAny(rel, asset.include) && !matchesAny(basename18(file), asset.include)) continue;
7084
- await copyAssetFile(file, join28(dest, rel), asset);
7148
+ if (!matchesAny(rel, asset.include) && !matchesAny(basename19(file), asset.include)) continue;
7149
+ await copyAssetFile(file, join29(dest, rel), asset);
7085
7150
  }
7086
7151
  }
7087
7152
  async function copyAssetFile(source, dest, asset) {
7088
- await mkdir17(dirname22(dest), { recursive: true });
7153
+ await mkdir18(dirname23(dest), { recursive: true });
7089
7154
  await cp5(source, dest, { dereference: true });
7090
7155
  if (asset.mode === "copy") await chmod(dest, 420);
7091
7156
  }
@@ -7101,7 +7166,7 @@ async function listFiles(root) {
7101
7166
  const out = [];
7102
7167
  async function walk2(dir) {
7103
7168
  for (const entry of (await readdir5(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
7104
- const full = join28(dir, entry.name);
7169
+ const full = join29(dir, entry.name);
7105
7170
  if (entry.isDirectory()) {
7106
7171
  await walk2(full);
7107
7172
  } else if (entry.isFile()) {
@@ -7120,7 +7185,7 @@ async function normalizeCopiedModes(path) {
7120
7185
  }
7121
7186
  if (!stats.isDirectory()) return;
7122
7187
  for (const entry of await readdir5(path, { withFileTypes: true })) {
7123
- await normalizeCopiedModes(join28(path, entry.name));
7188
+ await normalizeCopiedModes(join29(path, entry.name));
7124
7189
  }
7125
7190
  }
7126
7191
  function matchesAny(path, patterns) {
@@ -7133,10 +7198,108 @@ function matchesGlob(path, pattern) {
7133
7198
  }
7134
7199
 
7135
7200
  // src/model/workspace.ts
7136
- import { readFile as readFile21 } from "fs/promises";
7201
+ import { readFile as readFile22 } from "fs/promises";
7137
7202
  import { homedir as homedir4 } from "os";
7138
- import { dirname as dirname23, join as join29, resolve as resolve13 } from "path";
7203
+ import { dirname as dirname24, join as join30, resolve as resolve13 } from "path";
7139
7204
  import { z as z6 } from "zod";
7205
+
7206
+ // src/resolve/semver.ts
7207
+ var semverPattern = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
7208
+ function parseSemver(value) {
7209
+ const match = semverPattern.exec(value.trim());
7210
+ if (!match) return void 0;
7211
+ return {
7212
+ major: Number(match[1]),
7213
+ minor: Number(match[2]),
7214
+ patch: Number(match[3]),
7215
+ prerelease: match[4]
7216
+ };
7217
+ }
7218
+ function satisfiesVersionRange(version, range) {
7219
+ if (!range || range.trim() === "" || range.trim() === "*") return true;
7220
+ const parsedVersion = parseSemver(version);
7221
+ const trimmed = range.trim();
7222
+ if (!parsedVersion) {
7223
+ return trimmed === "*" || trimmed === version;
7224
+ }
7225
+ const comparators = parseRange(trimmed);
7226
+ if (!comparators) return trimmed === version;
7227
+ return comparators.every((comparator) => compareWith(parsedVersion, comparator));
7228
+ }
7229
+ function semverMajorOrVersion(version) {
7230
+ const parsed = parseSemver(version);
7231
+ return parsed ? String(parsed.major) : version;
7232
+ }
7233
+ function compareSemverStrings(a, b) {
7234
+ const parsedA = parseSemver(a);
7235
+ const parsedB = parseSemver(b);
7236
+ if (!parsedA || !parsedB) return a.localeCompare(b);
7237
+ return compareSemver(parsedA, parsedB);
7238
+ }
7239
+ function isSupportedVersionRange(range) {
7240
+ const trimmed = range.trim();
7241
+ return trimmed === "*" || parseRange(trimmed) !== void 0;
7242
+ }
7243
+ function parseRange(range) {
7244
+ if (range === "*") return [];
7245
+ if (range.startsWith("^")) {
7246
+ const base = parseSemver(range.slice(1));
7247
+ if (!base) return void 0;
7248
+ return [
7249
+ { op: ">=", version: base },
7250
+ { op: "<", version: caretUpperBound(base) }
7251
+ ];
7252
+ }
7253
+ if (range.startsWith("~")) {
7254
+ const base = parseSemver(range.slice(1));
7255
+ if (!base) return void 0;
7256
+ return [
7257
+ { op: ">=", version: base },
7258
+ { op: "<", version: { major: base.major, minor: base.minor + 1, patch: 0 } }
7259
+ ];
7260
+ }
7261
+ const parts = range.split(/\s+/).filter(Boolean);
7262
+ const comparators = [];
7263
+ for (const part of parts) {
7264
+ const match = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
7265
+ if (!match) return void 0;
7266
+ const version = parseSemver(match[2] ?? "");
7267
+ if (!version) return void 0;
7268
+ comparators.push({ op: match[1] ?? "=", version });
7269
+ }
7270
+ return comparators;
7271
+ }
7272
+ function caretUpperBound(version) {
7273
+ if (version.major > 0) return { major: version.major + 1, minor: 0, patch: 0 };
7274
+ if (version.minor > 0) return { major: 0, minor: version.minor + 1, patch: 0 };
7275
+ return { major: 0, minor: 0, patch: version.patch + 1 };
7276
+ }
7277
+ function compareWith(version, comparator) {
7278
+ const order = compareSemver(version, comparator.version);
7279
+ switch (comparator.op) {
7280
+ case "=":
7281
+ return order === 0;
7282
+ case ">":
7283
+ return order > 0;
7284
+ case ">=":
7285
+ return order >= 0;
7286
+ case "<":
7287
+ return order < 0;
7288
+ case "<=":
7289
+ return order <= 0;
7290
+ }
7291
+ }
7292
+ function compareSemver(a, b) {
7293
+ for (const key of ["major", "minor", "patch"]) {
7294
+ if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
7295
+ }
7296
+ if (a.prerelease === b.prerelease) return 0;
7297
+ if (!a.prerelease) return 1;
7298
+ if (!b.prerelease) return -1;
7299
+ return a.prerelease.localeCompare(b.prerelease);
7300
+ }
7301
+
7302
+ // src/model/workspace.ts
7140
7303
  var artifactSelectorListSchema = z6.array(z6.string().min(1));
7141
7304
  var workspaceSelectionImportSchema = z6.object({
7142
7305
  export: z6.string().min(1),
@@ -7177,6 +7340,9 @@ var workspacePackageBaseSchema = z6.object({
7177
7340
  adapterCodeHash: z6.string().min(16).optional(),
7178
7341
  installationType: installationTypeSchema.optional(),
7179
7342
  mode: z6.enum(["pinned", "tracking"]).default("pinned"),
7343
+ version: z6.string().min(1).refine(isSupportedVersionRange, {
7344
+ message: "Version policy must be an exact semver, ~range, ^range, comparator range, or *"
7345
+ }).optional(),
7180
7346
  requestedRef: z6.string().min(1).optional(),
7181
7347
  select: z6.array(z6.string().min(1)).optional(),
7182
7348
  skills: z6.array(z6.string().min(1)).optional(),
@@ -7212,9 +7378,45 @@ var workspaceProfileRuntimeSchema = z6.object({
7212
7378
  reloadRuntimes: z6.boolean().optional(),
7213
7379
  reloadCommands: commandListSchema
7214
7380
  });
7215
- var workspaceProfileSchema = z6.object({
7216
- runtimes: z6.array(workspaceProfileRuntimeSchema).min(1)
7381
+ var workspaceProfileMemberSchema = z6.object({
7382
+ id: z6.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i),
7383
+ workspace: z6.string().min(1),
7384
+ profile: z6.string().min(1),
7385
+ transport: z6.enum(["local", "ssh"]).default("local"),
7386
+ host: z6.string().min(1).optional(),
7387
+ user: z6.string().min(1).optional(),
7388
+ port: z6.number().int().positive().optional(),
7389
+ identityFile: z6.string().min(1).optional(),
7390
+ refreshTtlSeconds: z6.number().int().positive().optional()
7391
+ }).strict().superRefine((member, ctx) => {
7392
+ if (member.transport === "ssh" && !member.host) {
7393
+ ctx.addIssue({ code: "custom", path: ["host"], message: "SSH profile members require host" });
7394
+ }
7395
+ if (member.transport === "ssh" && !member.workspace.startsWith("/")) {
7396
+ ctx.addIssue({ code: "custom", path: ["workspace"], message: "SSH profile member workspaces must be absolute" });
7397
+ }
7398
+ });
7399
+ var workspaceLeafProfileSchema = z6.object({
7400
+ runtimes: z6.array(workspaceProfileRuntimeSchema).min(1),
7401
+ members: z6.never().optional()
7402
+ }).strict();
7403
+ var workspaceCompositeProfileSchema = z6.object({
7404
+ members: z6.array(workspaceProfileMemberSchema).min(1),
7405
+ runtimes: z6.never().optional(),
7406
+ refreshTtlSeconds: z6.number().int().positive().default(86400)
7407
+ }).strict().superRefine((profile, ctx) => {
7408
+ const seen = /* @__PURE__ */ new Set();
7409
+ for (const [index, member] of profile.members.entries()) {
7410
+ if (seen.has(member.id)) {
7411
+ ctx.addIssue({ code: "custom", path: ["members", index, "id"], message: "Composite profile member ids must be unique" });
7412
+ }
7413
+ seen.add(member.id);
7414
+ }
7217
7415
  });
7416
+ var workspaceProfileSchema = z6.union([
7417
+ workspaceLeafProfileSchema,
7418
+ workspaceCompositeProfileSchema
7419
+ ]);
7218
7420
  var workspaceRegistrySchema = z6.object({
7219
7421
  sources: z6.array(z6.string().min(1)).optional(),
7220
7422
  ttlSeconds: z6.number().int().positive().optional()
@@ -7269,12 +7471,12 @@ var workspaceConfigSchema = z6.discriminatedUnion("schemaVersion", [
7269
7471
  workspaceConfigV2Schema
7270
7472
  ]);
7271
7473
  function workspaceConfigPath(workspaceRoot) {
7272
- return join29(workspaceRoot, ".agentwheel", "config.json");
7474
+ return join30(workspaceRoot, ".agentwheel", "config.json");
7273
7475
  }
7274
7476
  async function readWorkspaceConfig(workspaceRoot) {
7275
7477
  const path = workspaceConfigPath(workspaceRoot);
7276
7478
  if (!await pathExists(path)) return emptyWorkspaceConfig();
7277
- return workspaceConfigSchema.parse(JSON.parse(await readFile21(path, "utf8")));
7479
+ return workspaceConfigSchema.parse(JSON.parse(await readFile22(path, "utf8")));
7278
7480
  }
7279
7481
  async function writeWorkspaceConfig(workspaceRoot, config) {
7280
7482
  await writeJsonAtomic(workspaceConfigPath(workspaceRoot), workspaceConfigSchema.parse(config));
@@ -7287,13 +7489,13 @@ function upsertPackage(config, entry) {
7287
7489
  return workspaceConfigSchema.parse({ ...parsed, packages });
7288
7490
  }
7289
7491
  function globalWorkspaceConfigPath(globalRoot = homedir4()) {
7290
- return join29(globalRoot, ".agentwheel", "config.json");
7492
+ return join30(globalRoot, ".agentwheel", "config.json");
7291
7493
  }
7292
7494
  async function findWorkspaceRoot(start = process.cwd()) {
7293
7495
  let current = resolve13(start);
7294
7496
  while (true) {
7295
7497
  if (await pathExists(workspaceConfigPath(current))) return current;
7296
- const parent = dirname23(current);
7498
+ const parent = dirname24(current);
7297
7499
  if (parent === current) return resolve13(start);
7298
7500
  current = parent;
7299
7501
  }
@@ -7330,9 +7532,12 @@ function resolveConfigPath(path, baseRoot) {
7330
7532
  function emptyWorkspaceConfig() {
7331
7533
  return { schemaVersion: 1, packages: [], registry: {}, trust: {}, profiles: {}, agents: {} };
7332
7534
  }
7535
+ function isCompositeWorkspaceProfile(profile) {
7536
+ return "members" in profile && Array.isArray(profile.members);
7537
+ }
7333
7538
  async function readConfigPath(path) {
7334
7539
  if (!await pathExists(path)) return emptyWorkspaceConfig();
7335
- return workspaceConfigSchema.parse(JSON.parse(await readFile21(path, "utf8")));
7540
+ return workspaceConfigSchema.parse(JSON.parse(await readFile22(path, "utf8")));
7336
7541
  }
7337
7542
  function mergeWorkspaceTrust(global, project) {
7338
7543
  return {
@@ -7347,18 +7552,18 @@ function sortedUnique2(values) {
7347
7552
  }
7348
7553
 
7349
7554
  // src/lifecycle/customization.ts
7350
- import { appendFile, cp as cp6, mkdir as mkdir18, rm as rm9 } from "fs/promises";
7351
- import { dirname as dirname25, join as join32 } from "path";
7555
+ import { appendFile, cp as cp6, mkdir as mkdir19, rm as rm9 } from "fs/promises";
7556
+ import { dirname as dirname26, join as join33 } from "path";
7352
7557
 
7353
7558
  // src/resolve/graph.ts
7354
7559
  import { createHash as createHash8 } from "crypto";
7355
- import { mkdtemp as mkdtemp4, readdir as readdir6, readFile as readFile24, stat as stat10 } from "fs/promises";
7560
+ import { mkdtemp as mkdtemp4, readdir as readdir6, readFile as readFile25, stat as stat10 } from "fs/promises";
7356
7561
  import { tmpdir as tmpdir5 } from "os";
7357
- import { basename as basename19, extname as extname4, join as join31 } from "path";
7562
+ import { basename as basename20, extname as extname4, join as join32 } from "path";
7358
7563
 
7359
7564
  // src/model/workspace-composition.ts
7360
7565
  import { createHash as createHash7 } from "crypto";
7361
- import { readFile as readFile22 } from "fs/promises";
7566
+ import { readFile as readFile23 } from "fs/promises";
7362
7567
  import { z as z7 } from "zod";
7363
7568
  var selectionSourceConfigSchema = z7.object({
7364
7569
  schemaVersion: z7.literal(2),
@@ -7377,7 +7582,7 @@ async function resolveSelectionImport(sourceRoot, sourceDriver, selection) {
7377
7582
  }
7378
7583
  let raw;
7379
7584
  try {
7380
- raw = JSON.parse(await readFile22(path, "utf8"));
7585
+ raw = JSON.parse(await readFile23(path, "utf8"));
7381
7586
  } catch (error) {
7382
7587
  const message = error instanceof Error ? error.message : String(error);
7383
7588
  throw new Error(`Selection import '${parsedSelection.export}' cannot parse ${path}: ${message}`);
@@ -7479,9 +7684,9 @@ import { homedir as homedir6 } from "os";
7479
7684
  import { resolve as resolve15 } from "path";
7480
7685
 
7481
7686
  // src/registry/client.ts
7482
- import { readFile as readFile23, rm as rm8, stat as stat9 } from "fs/promises";
7687
+ import { readFile as readFile24, rm as rm8, stat as stat9 } from "fs/promises";
7483
7688
  import { homedir as homedir5 } from "os";
7484
- import { dirname as dirname24, join as join30, resolve as resolve14 } from "path";
7689
+ import { dirname as dirname25, join as join31, resolve as resolve14 } from "path";
7485
7690
  import { fileURLToPath } from "url";
7486
7691
 
7487
7692
  // src/model/registry.ts
@@ -7585,7 +7790,7 @@ var RegistryClient = class {
7585
7790
  }
7586
7791
  async readCache() {
7587
7792
  if (!await pathExists(this.cachePath)) return void 0;
7588
- return registryCacheSchema.parse(JSON.parse(await readFile23(this.cachePath, "utf8")));
7793
+ return registryCacheSchema.parse(JSON.parse(await readFile24(this.cachePath, "utf8")));
7589
7794
  }
7590
7795
  isExpired(cache, ttlMs) {
7591
7796
  return this.now().getTime() - new Date(cache.fetchedAt).getTime() > ttlMs;
@@ -7604,10 +7809,10 @@ var RegistryClient = class {
7604
7809
  if (await pathExists(filePath)) {
7605
7810
  const fullPath = resolve14(filePath);
7606
7811
  const stats = await stat9(fullPath);
7607
- return readFile23(stats.isDirectory() ? join30(fullPath, "index.json") : fullPath, "utf8");
7812
+ return readFile24(stats.isDirectory() ? join31(fullPath, "index.json") : fullPath, "utf8");
7608
7813
  }
7609
- const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join30(dirname24(this.cachePath), "registry-repos") }));
7610
- return readFile23(join30(resolved.resolvedPath, "index.json"), "utf8");
7814
+ const resolved = await this.git.fetch(await this.git.resolve(source, { cacheRoot: join31(dirname25(this.cachePath), "registry-repos") }));
7815
+ return readFile24(join31(resolved.resolvedPath, "index.json"), "utf8");
7611
7816
  }
7612
7817
  warnCompatibility(entries) {
7613
7818
  for (const entry of entries) {
@@ -7645,7 +7850,7 @@ function mergeIndexes(indexes) {
7645
7850
  return [...merged.values()].sort((a, b) => a.name.localeCompare(b.name));
7646
7851
  }
7647
7852
  function defaultRegistryCachePath() {
7648
- return join30(homedir5(), ".agentwheel", "registry-cache.json");
7853
+ return join31(homedir5(), ".agentwheel", "registry-cache.json");
7649
7854
  }
7650
7855
  function sameSources(a, b) {
7651
7856
  return a.length === b.length && a.every((source, index) => source === b[index]);
@@ -7805,97 +8010,11 @@ function normalizeLiteralProviderSpec(source, prefix) {
7805
8010
  return `${prefix}${spec}`;
7806
8011
  }
7807
8012
 
7808
- // src/resolve/semver.ts
7809
- var semverPattern = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
7810
- function parseSemver(value) {
7811
- const match = semverPattern.exec(value.trim());
7812
- if (!match) return void 0;
7813
- return {
7814
- major: Number(match[1]),
7815
- minor: Number(match[2]),
7816
- patch: Number(match[3]),
7817
- prerelease: match[4]
7818
- };
7819
- }
7820
- function satisfiesVersionRange(version, range) {
7821
- if (!range || range.trim() === "" || range.trim() === "*") return true;
7822
- const parsedVersion = parseSemver(version);
7823
- const trimmed = range.trim();
7824
- if (!parsedVersion) {
7825
- return trimmed === "*" || trimmed === version;
7826
- }
7827
- const comparators = parseRange(trimmed);
7828
- if (!comparators) return trimmed === version;
7829
- return comparators.every((comparator) => compareWith(parsedVersion, comparator));
7830
- }
7831
- function semverMajorOrVersion(version) {
7832
- const parsed = parseSemver(version);
7833
- return parsed ? String(parsed.major) : version;
7834
- }
7835
- function parseRange(range) {
7836
- if (range === "*") return [];
7837
- if (range.startsWith("^")) {
7838
- const base = parseSemver(range.slice(1));
7839
- if (!base) return void 0;
7840
- return [
7841
- { op: ">=", version: base },
7842
- { op: "<", version: caretUpperBound(base) }
7843
- ];
7844
- }
7845
- if (range.startsWith("~")) {
7846
- const base = parseSemver(range.slice(1));
7847
- if (!base) return void 0;
7848
- return [
7849
- { op: ">=", version: base },
7850
- { op: "<", version: { major: base.major, minor: base.minor + 1, patch: 0 } }
7851
- ];
7852
- }
7853
- const parts = range.split(/\s+/).filter(Boolean);
7854
- const comparators = [];
7855
- for (const part of parts) {
7856
- const match = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
7857
- if (!match) return void 0;
7858
- const version = parseSemver(match[2] ?? "");
7859
- if (!version) return void 0;
7860
- comparators.push({ op: match[1] ?? "=", version });
7861
- }
7862
- return comparators;
7863
- }
7864
- function caretUpperBound(version) {
7865
- if (version.major > 0) return { major: version.major + 1, minor: 0, patch: 0 };
7866
- if (version.minor > 0) return { major: 0, minor: version.minor + 1, patch: 0 };
7867
- return { major: 0, minor: 0, patch: version.patch + 1 };
7868
- }
7869
- function compareWith(version, comparator) {
7870
- const order = compareSemver(version, comparator.version);
7871
- switch (comparator.op) {
7872
- case "=":
7873
- return order === 0;
7874
- case ">":
7875
- return order > 0;
7876
- case ">=":
7877
- return order >= 0;
7878
- case "<":
7879
- return order < 0;
7880
- case "<=":
7881
- return order <= 0;
7882
- }
7883
- }
7884
- function compareSemver(a, b) {
7885
- for (const key of ["major", "minor", "patch"]) {
7886
- if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
7887
- }
7888
- if (a.prerelease === b.prerelease) return 0;
7889
- if (!a.prerelease) return 1;
7890
- if (!b.prerelease) return -1;
7891
- return a.prerelease.localeCompare(b.prerelease);
7892
- }
7893
-
7894
8013
  // src/resolve/graph.ts
7895
8014
  var cacheLocks = /* @__PURE__ */ new Map();
7896
8015
  async function resolveDependencyGraph(roots, options) {
7897
8016
  if (roots.length === 0) throw new Error("At least one graph root is required.");
7898
- const graphRoot = await mkdtemp4(join31(tmpdir5(), "agentwheel-graph-"));
8017
+ const graphRoot = await mkdtemp4(join32(tmpdir5(), "agentwheel-graph-"));
7899
8018
  const fetchCache = /* @__PURE__ */ new Map();
7900
8019
  const nodesByKey = /* @__PURE__ */ new Map();
7901
8020
  const rootResults = [];
@@ -7908,6 +8027,7 @@ async function resolveDependencyGraph(roots, options) {
7908
8027
  select: root.select,
7909
8028
  selection: root.selection,
7910
8029
  mode: root.mode ?? "pinned",
8030
+ version: root.version,
7911
8031
  ref: root.ref,
7912
8032
  declaringPackageRoot: options.workspaceRoot,
7913
8033
  requiredBy: `workspace:${rootId}`,
@@ -8414,7 +8534,7 @@ async function collectIncludeNeeds(artifact, artifactsByRelativePath) {
8414
8534
  const file = stack.shift();
8415
8535
  if (scanned.has(file)) continue;
8416
8536
  scanned.add(file);
8417
- const content = await readFile24(file, "utf8");
8537
+ const content = await readFile25(file, "utf8");
8418
8538
  for (const include of extractOpenPackIncludeSelectors(content)) {
8419
8539
  await collectIncludeSelector(include.raw, include.optional, artifactsByRelativePath, scanned, stack, needs);
8420
8540
  }
@@ -8457,7 +8577,7 @@ async function listMarkdownFiles2(root) {
8457
8577
  const out = [];
8458
8578
  async function walk2(dir) {
8459
8579
  for (const entry of (await readdir6(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
8460
- const full = join31(dir, entry.name);
8580
+ const full = join32(dir, entry.name);
8461
8581
  if (entry.isDirectory()) {
8462
8582
  await walk2(full);
8463
8583
  } else if (entry.isFile() && extname4(entry.name).toLowerCase() === ".md") {
@@ -8492,7 +8612,7 @@ async function fetchPackage(normalized, mode, options, fetchCache, refOverride)
8492
8612
  const promise = (async () => {
8493
8613
  const driver = getSourceDriver(normalized.driver);
8494
8614
  const resolved = await driver.resolve(normalized.source, {
8495
- cacheRoot: options.cacheRoot ?? join31(options.workspaceRoot, ".agentwheel", "cache"),
8615
+ cacheRoot: options.cacheRoot ?? join32(options.workspaceRoot, ".agentwheel", "cache"),
8496
8616
  mode,
8497
8617
  ref: refOverride ?? normalized.requestedRef,
8498
8618
  frozenLock: hardLockedCheckout
@@ -8502,7 +8622,7 @@ async function fetchPackage(normalized, mode, options, fetchCache, refOverride)
8502
8622
  const exported = await driver.export(translated);
8503
8623
  const manifest = await readPackageManifest(exported.resolvedPath);
8504
8624
  const artifacts = await driver.list(exported);
8505
- const name = manifest?.name ?? exported.packageName ?? basename19(exported.resolvedPath);
8625
+ const name = manifest?.name ?? exported.packageName ?? basename20(exported.resolvedPath);
8506
8626
  const version = manifest?.version ?? exported.packageVersion ?? "0.0.0";
8507
8627
  const sourceHash = exported.sourceHash ?? await hashPath(exported.resolvedPath);
8508
8628
  return {
@@ -8609,8 +8729,8 @@ function verifyIntegrity(integrity, sourceHash, label) {
8609
8729
  async function withCachePathLock(path, fn) {
8610
8730
  const previous = cacheLocks.get(path) ?? Promise.resolve();
8611
8731
  let release = () => void 0;
8612
- const current = previous.then(() => new Promise((resolve21) => {
8613
- release = resolve21;
8732
+ const current = previous.then(() => new Promise((resolve23) => {
8733
+ release = resolve23;
8614
8734
  }));
8615
8735
  cacheLocks.set(path, current);
8616
8736
  await previous;
@@ -8699,8 +8819,8 @@ async function mapLimit(items, limit, fn) {
8699
8819
 
8700
8820
  // src/lifecycle/customization.ts
8701
8821
  async function remember(workspaceRoot, runtime, text) {
8702
- const overlayPath = join32(workspaceRoot, ".agentwheel", "overlays", runtime, "instructions.local.md");
8703
- await mkdir18(dirname25(overlayPath), { recursive: true });
8822
+ const overlayPath = join33(workspaceRoot, ".agentwheel", "overlays", runtime, "instructions.local.md");
8823
+ await mkdir19(dirname26(overlayPath), { recursive: true });
8704
8824
  await appendFile(overlayPath, `${text.trim()}
8705
8825
  `, "utf8");
8706
8826
  return { overlayPath };
@@ -8723,8 +8843,8 @@ async function ejectArtifact(workspaceRoot, item) {
8723
8843
  throw new Error(`Artifact not found: ${item}`);
8724
8844
  }
8725
8845
  const ejectedIdentity = parsed.packageIdentity === parsed.packageName ? parsed.packageIdentity : candidate.nodeId === parsed.packageIdentity ? candidate.nodeId : `${candidate.packageName}@${candidate.packageVersion}`;
8726
- const ejectedPath = join32(workspaceRoot, ".agentwheel", "ejected", ...ejectedIdentity.split("/"), parsed.type, parsed.name);
8727
- await mkdir18(dirname25(ejectedPath), { recursive: true });
8846
+ const ejectedPath = join33(workspaceRoot, ".agentwheel", "ejected", ...ejectedIdentity.split("/"), parsed.type, parsed.name);
8847
+ await mkdir19(dirname26(ejectedPath), { recursive: true });
8728
8848
  await rm9(ejectedPath, { recursive: true, force: true });
8729
8849
  await cp6(artifact.stagedPath ?? artifact.sourcePath, ejectedPath, { recursive: artifact.kind === "dir", dereference: true });
8730
8850
  return {
@@ -8766,7 +8886,7 @@ async function stageEjectCandidate(workspaceRoot, pkg) {
8766
8886
  const adapter = pkg.adapterConfig ? await loadAdapterConfig(pkg.adapterConfig) : getAdapter(pkg.adapter);
8767
8887
  const bundle = await stageSource(driver, normalized.source, {
8768
8888
  adapter,
8769
- cacheRoot: join32(workspaceRoot, ".agentwheel", "cache"),
8889
+ cacheRoot: join33(workspaceRoot, ".agentwheel", "cache"),
8770
8890
  mode: pkg.mode,
8771
8891
  ref: normalized.requestedRef ?? pkg.requestedRef
8772
8892
  });
@@ -8817,8 +8937,8 @@ import { rm as rm10 } from "fs/promises";
8817
8937
 
8818
8938
  // src/lifecycle/source-plan.ts
8819
8939
  import { createHash as createHash10 } from "crypto";
8820
- import { mkdir as mkdir20 } from "fs/promises";
8821
- import { dirname as dirname27, join as join35 } from "path";
8940
+ import { mkdir as mkdir21 } from "fs/promises";
8941
+ import { dirname as dirname28, join as join36 } from "path";
8822
8942
 
8823
8943
  // src/resolve/graph-diff.ts
8824
8944
  function diffGraphLocks(previous, next) {
@@ -8980,11 +9100,11 @@ function formatSelectionImport(root) {
8980
9100
 
8981
9101
  // src/resolve/render.ts
8982
9102
  import { createHash as createHash9 } from "crypto";
8983
- import { readFile as readFile25, mkdtemp as mkdtemp5 } from "fs/promises";
9103
+ import { readFile as readFile26, mkdtemp as mkdtemp5 } from "fs/promises";
8984
9104
  import { tmpdir as tmpdir6 } from "os";
8985
- import { join as join33 } from "path";
9105
+ import { join as join34 } from "path";
8986
9106
  async function renderGraphForTarget(graph, targetContext = {}) {
8987
- const root = await mkdtemp5(join33(tmpdir6(), "agentwheel-render-"));
9107
+ const root = await mkdtemp5(join34(tmpdir6(), "agentwheel-render-"));
8988
9108
  const artifacts = [];
8989
9109
  const stagedNodes = /* @__PURE__ */ new Map();
8990
9110
  const includeEdges = /* @__PURE__ */ new Map();
@@ -9056,7 +9176,8 @@ async function renderGraphForTarget(graph, targetContext = {}) {
9056
9176
  const selectedArtifacts = filterArtifactsBySelection(expandedArtifacts, rawNode.node.selected);
9057
9177
  const runtimeSelectedSet = new Set(normalizeArtifactSelectors(rawNode.node.selected) ?? []);
9058
9178
  const runtimeArtifacts = targetContext.adapter ? filterArtifactsByRuntime2(selectedArtifacts, targetContext.adapter.name, runtimeSelectedSet) : selectedArtifacts;
9059
- const codexRenderedArtifacts = await renderCodexSubagents(runtimeArtifacts, staged.root, targetContext.adapter);
9179
+ const claudeRenderedArtifacts = await renderClaudeSubagents(runtimeArtifacts, staged.root, targetContext.adapter);
9180
+ const codexRenderedArtifacts = await renderCodexSubagents(claudeRenderedArtifacts, staged.root, targetContext.adapter);
9060
9181
  const openClawRenderedArtifacts = await renderOpenClawSubagents(codexRenderedArtifacts, staged.root, targetContext.adapter);
9061
9182
  const runtimeRenderedArtifacts = await renderCopilotArtifacts(openClawRenderedArtifacts, staged.root, targetContext.adapter);
9062
9183
  const renderedArtifacts = targetContext.workspaceRoot && targetContext.adapter ? await applyCustomizations(runtimeRenderedArtifacts, {
@@ -9105,7 +9226,7 @@ async function artifactContentMap(artifacts) {
9105
9226
  const out = /* @__PURE__ */ new Map();
9106
9227
  for (const artifact of artifacts) {
9107
9228
  if (artifact.kind !== "file") continue;
9108
- out.set(artifact.relativePath.replaceAll("\\", "/"), await readFile25(artifact.stagedPath ?? artifact.sourcePath, "utf8"));
9229
+ out.set(artifact.relativePath.replaceAll("\\", "/"), await readFile26(artifact.stagedPath ?? artifact.sourcePath, "utf8"));
9109
9230
  }
9110
9231
  return out;
9111
9232
  }
@@ -9368,9 +9489,9 @@ function lockArtifactFor(artifact) {
9368
9489
  }
9369
9490
 
9370
9491
  // src/lifecycle/trust.ts
9371
- import { mkdir as mkdir19, readFile as readFile26 } from "fs/promises";
9492
+ import { mkdir as mkdir20, readFile as readFile27 } from "fs/promises";
9372
9493
  import { homedir as homedir7 } from "os";
9373
- import { dirname as dirname26, join as join34 } from "path";
9494
+ import { dirname as dirname27, join as join35 } from "path";
9374
9495
  import { z as z9 } from "zod";
9375
9496
  var trustStoreSchema = z9.object({
9376
9497
  version: z9.literal(1),
@@ -9444,14 +9565,14 @@ function sortedUnique5(values) {
9444
9565
  }
9445
9566
  async function readTrustStore(path) {
9446
9567
  if (!await pathExists(path)) return { version: 1, acceptedSources: [] };
9447
- return trustStoreSchema.parse(JSON.parse(await readFile26(path, "utf8")));
9568
+ return trustStoreSchema.parse(JSON.parse(await readFile27(path, "utf8")));
9448
9569
  }
9449
9570
  async function writeTrustStore(path, store) {
9450
- await mkdir19(dirname26(path), { recursive: true });
9571
+ await mkdir20(dirname27(path), { recursive: true });
9451
9572
  await writeJsonAtomic(path, trustStoreSchema.parse(store));
9452
9573
  }
9453
9574
  function defaultTrustStorePath() {
9454
- return process.env.AGENTWHEEL_TRUST_STORE ?? join34(homedir7(), ".agentwheel", "trust.json");
9575
+ return process.env.AGENTWHEEL_TRUST_STORE ?? join35(homedir7(), ".agentwheel", "trust.json");
9455
9576
  }
9456
9577
 
9457
9578
  // src/lifecycle/ownership.ts
@@ -9623,7 +9744,7 @@ async function createGraphSourcePlan(options) {
9623
9744
  const registryClient = new RegistryClient({ workspaceRoot, offline: lockMode, offlineLabel: lockLabel, warn });
9624
9745
  const graph = await resolveDependencyGraph(options.roots, {
9625
9746
  workspaceRoot,
9626
- cacheRoot: join35(workspaceRoot, ".agentwheel", "cache"),
9747
+ cacheRoot: join36(workspaceRoot, ".agentwheel", "cache"),
9627
9748
  registryClient,
9628
9749
  noDeps: options.noDeps,
9629
9750
  includeSuggestions: options.includeSuggestions,
@@ -9729,7 +9850,7 @@ async function readExistingGraphLock(path) {
9729
9850
  return readGraphLock(path);
9730
9851
  }
9731
9852
  function pathForGraphLock(workspaceRoot, targetKey2, adapter, targetFingerprint) {
9732
- return join35(workspaceRoot, ".agentwheel", "locks", sanitizePathSegment(targetKey2), sanitizePathSegment(adapter), `${targetFingerprint}.graph-lock.json`);
9853
+ return join36(workspaceRoot, ".agentwheel", "locks", sanitizePathSegment(targetKey2), sanitizePathSegment(adapter), `${targetFingerprint}.graph-lock.json`);
9733
9854
  }
9734
9855
  function sanitizePathSegment(value) {
9735
9856
  return value.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "") || "default";
@@ -9861,7 +9982,7 @@ function targetLabel(target) {
9861
9982
  }
9862
9983
 
9863
9984
  // src/runtime/target.ts
9864
- import { basename as basename20, dirname as dirname28, join as join36, resolve as resolve17 } from "path";
9985
+ import { basename as basename21, dirname as dirname29, join as join37, resolve as resolve17 } from "path";
9865
9986
  var runtimeMarkers = [
9866
9987
  { adapter: "openclaw", dirs: [".openclaw", ".clawdbot", ".moltbot"] },
9867
9988
  { adapter: "claude", dirs: [".claude"] },
@@ -9920,6 +10041,9 @@ async function resolveProfileRuntimeTargets(request) {
9920
10041
  if (!profile) {
9921
10042
  throw new Error(`Unknown profile: ${request.profile}`);
9922
10043
  }
10044
+ if (isCompositeWorkspaceProfile(profile)) {
10045
+ throw new Error(`Profile '${request.profile}' is composite and has no direct runtime targets.`);
10046
+ }
9923
10047
  return profile.runtimes.map((runtime) => resolveProfileRuntimeTarget(runtime, config, workspaceRoot, request.installationType));
9924
10048
  }
9925
10049
  function resolveProfileRuntimeTarget(runtime, config, workspaceRoot, installationType) {
@@ -9978,9 +10102,9 @@ async function detectRuntimeTargets(cwd = process.cwd(), adapterFilter) {
9978
10102
  for (const marker of runtimeMarkers) {
9979
10103
  if (adapterFilter && marker.adapter !== adapterFilter) continue;
9980
10104
  for (const dir of marker.dirs) {
9981
- if (basename20(root) === dir) {
9982
- matches.push({ adapter: marker.adapter, targetRoot: dirname28(root) });
9983
- } else if (await pathExists(join36(root, dir))) {
10105
+ if (basename21(root) === dir) {
10106
+ matches.push({ adapter: marker.adapter, targetRoot: dirname29(root) });
10107
+ } else if (await pathExists(join37(root, dir))) {
9984
10108
  matches.push({ adapter: marker.adapter, targetRoot: root });
9985
10109
  }
9986
10110
  }
@@ -10022,7 +10146,7 @@ function dedupeTargets(matches) {
10022
10146
  function runtimeScanRoot(request) {
10023
10147
  const root = resolve17(request.targetRoot ?? request.cwd ?? process.cwd());
10024
10148
  if (request.targetRoot) return root;
10025
- return runtimeMarkers.some((marker) => marker.dirs.includes(basename20(root))) ? dirname28(root) : root;
10149
+ return runtimeMarkers.some((marker) => marker.dirs.includes(basename21(root))) ? dirname29(root) : root;
10026
10150
  }
10027
10151
 
10028
10152
  // src/lifecycle/profile.ts
@@ -10032,6 +10156,9 @@ async function syncProfile(options) {
10032
10156
  if (!profile) {
10033
10157
  throw new Error(`Unknown profile: ${options.profile}`);
10034
10158
  }
10159
+ if (isCompositeWorkspaceProfile(profile)) {
10160
+ throw new Error(`Composite profile '${options.profile}' must be executed through member delegation.`);
10161
+ }
10035
10162
  const packages = options.source ? [await packageFromSource(options.source, options)] : config.packages;
10036
10163
  if (packages.length === 0) {
10037
10164
  throw new Error("Profile sync needs a source argument or configured packages.");
@@ -10059,6 +10186,7 @@ async function syncProfile(options) {
10059
10186
  rootId: pkg.name,
10060
10187
  source: pkg.source,
10061
10188
  mode: options.mode ?? pkg.mode,
10189
+ version: pkg.version,
10062
10190
  ref: pkg.requestedRef,
10063
10191
  select: pkg.selection ? void 0 : selected ?? normalizeArtifactSelectors(pkg.select, pkg.skills),
10064
10192
  selection: pkg.selection,
@@ -10320,9 +10448,9 @@ function shellQuoteArg(value) {
10320
10448
  }
10321
10449
 
10322
10450
  // src/cli/update-check.ts
10323
- import { mkdir as mkdir21, readFile as readFile27, writeFile as writeFile19 } from "fs/promises";
10451
+ import { mkdir as mkdir22, readFile as readFile28, writeFile as writeFile20 } from "fs/promises";
10324
10452
  import { homedir as homedir8 } from "os";
10325
- import { dirname as dirname29, join as join37 } from "path";
10453
+ import { dirname as dirname30, join as join38 } from "path";
10326
10454
  var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
10327
10455
  var DEFAULT_TIMEOUT_MS = 300;
10328
10456
  var REGISTRY_URL = "https://registry.npmjs.org/agentwheel";
@@ -10330,7 +10458,7 @@ async function maybeCheckForUpdate(options) {
10330
10458
  if (isDisabled(options)) return;
10331
10459
  const now = options.now?.() ?? /* @__PURE__ */ new Date();
10332
10460
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
10333
- const cachePath = options.cachePath ?? join37(homedir8(), ".agentwheel", "update-check.json");
10461
+ const cachePath = options.cachePath ?? join38(homedir8(), ".agentwheel", "update-check.json");
10334
10462
  try {
10335
10463
  const cached = await readCache(cachePath);
10336
10464
  if (cached && now.getTime() - Date.parse(cached.checkedAt) < ttlMs) {
@@ -10367,7 +10495,7 @@ async function fetchLatestVersion(fetchImpl, timeoutMs) {
10367
10495
  }
10368
10496
  async function readCache(path) {
10369
10497
  try {
10370
- const parsed = JSON.parse(await readFile27(path, "utf8"));
10498
+ const parsed = JSON.parse(await readFile28(path, "utf8"));
10371
10499
  if (typeof parsed.checkedAt !== "string" || typeof parsed.latest !== "string") return void 0;
10372
10500
  return { checkedAt: parsed.checkedAt, latest: parsed.latest };
10373
10501
  } catch {
@@ -10375,8 +10503,8 @@ async function readCache(path) {
10375
10503
  }
10376
10504
  }
10377
10505
  async function writeCache(path, cache) {
10378
- await mkdir21(dirname29(path), { recursive: true });
10379
- await writeFile19(path, `${JSON.stringify(cache, null, 2)}
10506
+ await mkdir22(dirname30(path), { recursive: true });
10507
+ await writeFile20(path, `${JSON.stringify(cache, null, 2)}
10380
10508
  `, "utf8");
10381
10509
  }
10382
10510
  function warnIfNewer(latest, current, stderr = process.stderr) {
@@ -10539,13 +10667,13 @@ function isCrossPackageSelector(value) {
10539
10667
  }
10540
10668
 
10541
10669
  // src/model/package-migrate.ts
10542
- import { readFile as readFile28, rename as rename4, writeFile as writeFile20 } from "fs/promises";
10543
- import { join as join39, resolve as resolve19 } from "path";
10670
+ import { readFile as readFile29, rename as rename4, writeFile as writeFile21 } from "fs/promises";
10671
+ import { join as join40, resolve as resolve19 } from "path";
10544
10672
  import { applyEdits, modify, parse as parse5 } from "jsonc-parser";
10545
10673
  async function migratePackageManifest(root) {
10546
10674
  const packageRoot = resolve19(root);
10547
10675
  for (const name of openPackManifestNames) {
10548
- const path = join39(packageRoot, name);
10676
+ const path = join40(packageRoot, name);
10549
10677
  if (await pathExists(path)) {
10550
10678
  return { changed: false, to: path, message: `Package already uses ${name}.` };
10551
10679
  }
@@ -10554,18 +10682,18 @@ async function migratePackageManifest(root) {
10554
10682
  if (!legacyName) {
10555
10683
  throw new Error(`No legacy package manifest found at ${packageRoot}`);
10556
10684
  }
10557
- const from = join39(packageRoot, legacyName);
10685
+ const from = join40(packageRoot, legacyName);
10558
10686
  const toName = legacyName.endsWith(".jsonc") ? "openpack.jsonc" : "openpack.json";
10559
- const to = join39(packageRoot, toName);
10560
- const content = await readFile28(from, "utf8");
10687
+ const to = join40(packageRoot, toName);
10688
+ const content = await readFile29(from, "utf8");
10561
10689
  const updated = updateSchemaVersion(content);
10562
10690
  await rename4(from, to);
10563
- await writeFile20(to, updated, "utf8");
10691
+ await writeFile21(to, updated, "utf8");
10564
10692
  return { changed: true, from, to, message: `Migrated ${legacyName} to ${toName}.` };
10565
10693
  }
10566
10694
  async function firstExistingLegacyManifest(root) {
10567
10695
  for (const name of legacyPackageManifestNames) {
10568
- if (await pathExists(join39(root, name))) return name;
10696
+ if (await pathExists(join40(root, name))) return name;
10569
10697
  }
10570
10698
  return void 0;
10571
10699
  }
@@ -10583,25 +10711,613 @@ function updateSchemaVersion(content) {
10583
10711
 
10584
10712
  // src/cli/version.ts
10585
10713
  import { readFileSync } from "fs";
10586
- import { dirname as dirname30, join as join40 } from "path";
10714
+ import { dirname as dirname31, join as join41 } from "path";
10587
10715
  import { fileURLToPath as fileURLToPath2 } from "url";
10588
10716
  var FALLBACK_VERSION = "0.0.0";
10589
10717
  function resolveCliVersion() {
10590
- let dir = dirname30(fileURLToPath2(import.meta.url));
10718
+ let dir = dirname31(fileURLToPath2(import.meta.url));
10591
10719
  while (true) {
10592
10720
  try {
10593
- const pkg = JSON.parse(readFileSync(join40(dir, "package.json"), "utf8"));
10721
+ const pkg = JSON.parse(readFileSync(join41(dir, "package.json"), "utf8"));
10594
10722
  if (pkg.name === "agentwheel" && typeof pkg.version === "string") {
10595
10723
  return pkg.version;
10596
10724
  }
10597
10725
  } catch {
10598
10726
  }
10599
- const parent = dirname30(dir);
10727
+ const parent = dirname31(dir);
10600
10728
  if (parent === dir) return FALLBACK_VERSION;
10601
10729
  dir = parent;
10602
10730
  }
10603
10731
  }
10604
10732
 
10733
+ // src/version/policy.ts
10734
+ import { execFile as execFile5 } from "child_process";
10735
+ import { readFile as readFile30 } from "fs/promises";
10736
+ import { join as join42, resolve as resolve20 } from "path";
10737
+ import { promisify as promisify5 } from "util";
10738
+ import { parse as parseJsonc } from "jsonc-parser";
10739
+ import { z as z10 } from "zod";
10740
+ var execFileAsync5 = promisify5(execFile5);
10741
+ var DEFAULT_VERSION_REFRESH_TTL_SECONDS = 86400;
10742
+ var cachedVersionSchema = z10.object({
10743
+ version: z10.string().min(1),
10744
+ ref: z10.string().min(1)
10745
+ });
10746
+ var versionCacheEntrySchema = z10.object({
10747
+ checkedAt: z10.string().datetime(),
10748
+ versions: z10.array(cachedVersionSchema)
10749
+ });
10750
+ var versionCacheSchema = z10.object({
10751
+ schemaVersion: z10.literal(1),
10752
+ sources: z10.record(z10.string(), versionCacheEntrySchema)
10753
+ });
10754
+ async function discoverPackageVersions(pkg, workspaceRoot, options = {}) {
10755
+ const now = (options.now ?? (() => /* @__PURE__ */ new Date()))();
10756
+ const ttlSeconds = options.ttlSeconds ?? DEFAULT_VERSION_REFRESH_TTL_SECONDS;
10757
+ const cachePath = versionCachePath(workspaceRoot);
10758
+ const cache = await readVersionCache(cachePath);
10759
+ const cached = cache.sources[pkg.source];
10760
+ const cachedAgeMs = cached ? now.getTime() - new Date(cached.checkedAt).getTime() : Number.POSITIVE_INFINITY;
10761
+ const cachedFresh = cachedAgeMs <= ttlSeconds * 1e3;
10762
+ if (options.offline || cached && cachedFresh && !options.forceRefresh) {
10763
+ return availabilityFromVersions(pkg, cached?.versions ?? [], {
10764
+ checkedAt: cached?.checkedAt ?? null,
10765
+ stale: !cachedFresh,
10766
+ refreshed: false,
10767
+ error: !cached && options.offline ? "No cached version index is available offline." : void 0
10768
+ });
10769
+ }
10770
+ try {
10771
+ const versions = await discoverVersionsFromSource(pkg, workspaceRoot);
10772
+ const checkedAt = now.toISOString();
10773
+ await writeJsonAtomic(cachePath, {
10774
+ schemaVersion: 1,
10775
+ sources: {
10776
+ ...cache.sources,
10777
+ [pkg.source]: { checkedAt, versions }
10778
+ }
10779
+ });
10780
+ return availabilityFromVersions(pkg, versions, {
10781
+ checkedAt,
10782
+ stale: false,
10783
+ refreshed: true
10784
+ });
10785
+ } catch (error) {
10786
+ const message = error instanceof Error ? error.message : String(error);
10787
+ return availabilityFromVersions(pkg, cached?.versions ?? [], {
10788
+ checkedAt: cached?.checkedAt ?? null,
10789
+ stale: true,
10790
+ refreshed: false,
10791
+ error: message
10792
+ });
10793
+ }
10794
+ }
10795
+ async function effectiveTrackingRef(pkg, workspaceRoot, options = {}) {
10796
+ if (pkg.mode !== "tracking" || !pkg.version) return { ref: pkg.requestedRef };
10797
+ const availability = await discoverPackageVersions(pkg, workspaceRoot, options);
10798
+ const driverName = pkg.driver === "local" ? inferSourceDriverName(pkg.source) : pkg.driver;
10799
+ return {
10800
+ ref: driverName === "local" ? pkg.requestedRef : availability.latestAllowedRef ?? pkg.requestedRef,
10801
+ availability
10802
+ };
10803
+ }
10804
+ function availabilityFromVersions(pkg, versions, state) {
10805
+ const sorted = [...versions].sort((a, b) => compareSemverStrings(b.version, a.version));
10806
+ const policy = pkg.version ?? "*";
10807
+ const latestOverall = sorted[0] ?? null;
10808
+ const latestAllowed = sorted.find((candidate) => satisfiesVersionRange(candidate.version, policy)) ?? null;
10809
+ return {
10810
+ source: pkg.source,
10811
+ policy,
10812
+ checkedAt: state.checkedAt,
10813
+ stale: state.stale,
10814
+ refreshed: state.refreshed,
10815
+ latestAllowed: latestAllowed?.version ?? null,
10816
+ latestAllowedRef: latestAllowed?.ref ?? null,
10817
+ latestOverall: latestOverall?.version ?? null,
10818
+ latestOverallRef: latestOverall?.ref ?? null,
10819
+ versions: sorted,
10820
+ ...state.error ? { error: state.error } : {}
10821
+ };
10822
+ }
10823
+ async function discoverVersionsFromSource(pkg, workspaceRoot) {
10824
+ const driverName = pkg.driver === "local" ? inferSourceDriverName(pkg.source) : pkg.driver;
10825
+ if (driverName === "git") {
10826
+ const tagged = await discoverGitTags(pkg.source, pkg.version);
10827
+ if (tagged.length > 0) return tagged;
10828
+ }
10829
+ if (driverName === "local") {
10830
+ const root = resolve20(workspaceRoot, pkg.source);
10831
+ const manifest2 = await readPackageManifest(root);
10832
+ const current = manifest2 ? [{ version: manifest2.version, ref: pkg.requestedRef ?? root }] : [];
10833
+ try {
10834
+ const { stdout } = await execFileAsync5("git", ["-C", root, "remote", "get-url", "origin"]);
10835
+ return uniqueVersions([
10836
+ ...await discoverGitTagsFromUrl(stdout.trim(), pkg.version, root),
10837
+ ...current
10838
+ ]);
10839
+ } catch {
10840
+ return current;
10841
+ }
10842
+ }
10843
+ const driver = getSourceDriver(driverName);
10844
+ const resolved = await driver.resolve(pkg.source, {
10845
+ cacheRoot: join42(workspaceRoot, ".agentwheel", "cache"),
10846
+ mode: "tracking",
10847
+ ref: pkg.requestedRef
10848
+ });
10849
+ const fetched = await driver.fetch(resolved);
10850
+ const manifest = await readPackageManifest(fetched.resolvedPath);
10851
+ const version = manifest?.version ?? fetched.packageVersion;
10852
+ return version ? [{ version, ref: fetched.requestedRef ?? pkg.requestedRef ?? "HEAD" }] : [];
10853
+ }
10854
+ async function discoverGitTags(source, policy) {
10855
+ const url = gitUrlFromSource(source);
10856
+ const localRoot = url.startsWith("/") ? url : void 0;
10857
+ return discoverGitTagsFromUrl(url, policy, localRoot);
10858
+ }
10859
+ async function discoverGitTagsFromUrl(url, policy, localRoot) {
10860
+ const { stdout } = await execFileAsync5("git", ["ls-remote", "--tags", "--refs", url], {
10861
+ maxBuffer: 10 * 1024 * 1024
10862
+ });
10863
+ const byVersion = /* @__PURE__ */ new Map();
10864
+ for (const line of stdout.split(/\r?\n/)) {
10865
+ const match = /^[0-9a-f]+\s+refs\/tags\/(.+)$/.exec(line.trim());
10866
+ if (!match) continue;
10867
+ const tag = match[1];
10868
+ if (!parseSemver(tag)) continue;
10869
+ const version = tag.replace(/^v/, "");
10870
+ const incumbent = byVersion.get(version);
10871
+ if (!incumbent || tag.startsWith("v")) byVersion.set(version, { version, ref: tag });
10872
+ }
10873
+ const candidates = [...byVersion.values()].sort((a, b) => compareSemverStrings(b.version, a.version));
10874
+ const valid = [];
10875
+ let foundOverall = false;
10876
+ let foundAllowed = false;
10877
+ for (const candidate of candidates) {
10878
+ const manifestVersion = await manifestVersionAtRef(url, candidate.ref, localRoot);
10879
+ if (manifestVersion !== candidate.version) continue;
10880
+ valid.push(candidate);
10881
+ foundOverall = true;
10882
+ if (satisfiesVersionRange(candidate.version, policy)) foundAllowed = true;
10883
+ if (foundOverall && foundAllowed) break;
10884
+ }
10885
+ return valid;
10886
+ }
10887
+ function uniqueVersions(versions) {
10888
+ const byVersion = /* @__PURE__ */ new Map();
10889
+ for (const version of versions) {
10890
+ if (!byVersion.has(version.version)) byVersion.set(version.version, version);
10891
+ }
10892
+ return [...byVersion.values()].sort((a, b) => compareSemverStrings(b.version, a.version));
10893
+ }
10894
+ async function manifestVersionAtRef(url, ref, localRoot) {
10895
+ if (localRoot) {
10896
+ for (const name of ["openpack.json", "openpack.jsonc"]) {
10897
+ try {
10898
+ const { stdout } = await execFileAsync5("git", ["-C", localRoot, "show", `${ref}:${name}`], {
10899
+ maxBuffer: 1024 * 1024
10900
+ });
10901
+ const parsed = parseJsonc(stdout);
10902
+ if (typeof parsed?.version === "string") return parsed.version.replace(/^v/, "");
10903
+ } catch {
10904
+ }
10905
+ }
10906
+ return null;
10907
+ }
10908
+ const repository = githubRepositoryFromUrl(url);
10909
+ if (!repository) return null;
10910
+ for (const name of ["openpack.json", "openpack.jsonc"]) {
10911
+ const response = await fetch(
10912
+ `https://raw.githubusercontent.com/${repository}/${encodeURIComponent(ref)}/${name}`,
10913
+ { headers: { "user-agent": "agentwheel-version-discovery" } }
10914
+ );
10915
+ if (!response.ok) continue;
10916
+ const parsed = parseJsonc(await response.text());
10917
+ if (typeof parsed?.version === "string") return parsed.version.replace(/^v/, "");
10918
+ }
10919
+ return null;
10920
+ }
10921
+ function githubRepositoryFromUrl(url) {
10922
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:)([^/]+\/[^/#]+?)(?:\.git)?$/.exec(url);
10923
+ return match?.[1] ?? null;
10924
+ }
10925
+ function gitUrlFromSource(source) {
10926
+ if (source.startsWith("github:")) {
10927
+ const repo = source.slice("github:".length).split("#", 1)[0];
10928
+ if (!repo.includes("/")) throw new Error(`Invalid GitHub source: ${source}`);
10929
+ return `https://github.com/${repo}.git`;
10930
+ }
10931
+ if (source.startsWith("git:")) {
10932
+ const rest = source.slice("git:".length);
10933
+ const hashIndex = rest.lastIndexOf("#");
10934
+ return hashIndex >= 0 ? rest.slice(0, hashIndex) : rest;
10935
+ }
10936
+ throw new Error(`Version discovery does not support Git source: ${source}`);
10937
+ }
10938
+ function versionCachePath(workspaceRoot) {
10939
+ return join42(workspaceRoot, ".agentwheel", "cache", "version-index.json");
10940
+ }
10941
+ async function readVersionCache(path) {
10942
+ if (!await pathExists(path)) return { schemaVersion: 1, sources: {} };
10943
+ try {
10944
+ return versionCacheSchema.parse(JSON.parse(await readFile30(path, "utf8")));
10945
+ } catch {
10946
+ return { schemaVersion: 1, sources: {} };
10947
+ }
10948
+ }
10949
+
10950
+ // src/profile/members.ts
10951
+ import { execFile as execFile6 } from "child_process";
10952
+ import { readFile as readFile31 } from "fs/promises";
10953
+ import { join as join43, resolve as resolve21 } from "path";
10954
+ import { promisify as promisify6 } from "util";
10955
+ import { z as z12 } from "zod";
10956
+
10957
+ // src/status/report.ts
10958
+ import { z as z11 } from "zod";
10959
+ var statusHealthSchema = z11.enum([
10960
+ "PASS",
10961
+ "WARN",
10962
+ "FAIL",
10963
+ "STALE",
10964
+ "DEGRADED",
10965
+ "INCOMPATIBLE",
10966
+ "BUSY"
10967
+ ]);
10968
+ var statusPackageSchema = z11.object({
10969
+ name: z11.string().min(1),
10970
+ source: z11.string().min(1),
10971
+ mode: z11.enum(["pinned", "tracking"]),
10972
+ policy: z11.string().min(1),
10973
+ installed: z11.string().nullable(),
10974
+ locked: z11.string().nullable(),
10975
+ latestAllowed: z11.string().nullable(),
10976
+ latestOverall: z11.string().nullable(),
10977
+ availability: z11.enum(["FRESH", "STALE", "UNKNOWN"]),
10978
+ checkedAt: z11.string().nullable(),
10979
+ error: z11.string().optional(),
10980
+ updateAvailableAllowed: z11.boolean(),
10981
+ updateAvailableOverall: z11.boolean()
10982
+ });
10983
+ var statusArtifactSchema = z11.object({
10984
+ selector: z11.string().min(1),
10985
+ type: z11.string().min(1),
10986
+ name: z11.string().min(1),
10987
+ installName: z11.string().min(1),
10988
+ packageName: z11.string().nullable(),
10989
+ packageVersion: z11.string().nullable(),
10990
+ hash: z11.string().min(16),
10991
+ installed: z11.boolean()
10992
+ });
10993
+ var statusTargetSchema = z11.object({
10994
+ adapter: z11.string().min(1),
10995
+ installationType: z11.string().min(1),
10996
+ targetRoot: z11.string().min(1),
10997
+ health: statusHealthSchema,
10998
+ manifestRevision: z11.string().nullable(),
10999
+ manifestEntryCount: z11.number().int().nonnegative(),
11000
+ graphLockPath: z11.string().nullable(),
11001
+ packageCount: z11.number().int().nonnegative(),
11002
+ artifactCount: z11.number().int().nonnegative(),
11003
+ pendingCount: z11.number().int().nonnegative(),
11004
+ driftCount: z11.number().int().nonnegative(),
11005
+ conflictCount: z11.number().int().nonnegative(),
11006
+ error: z11.string().optional(),
11007
+ packages: z11.array(statusPackageSchema),
11008
+ artifacts: z11.array(statusArtifactSchema)
11009
+ });
11010
+ var statusReportSchema = z11.lazy(() => z11.object({
11011
+ schemaVersion: z11.literal(1),
11012
+ command: z11.literal("status"),
11013
+ agentwheelVersion: z11.string().min(1),
11014
+ generatedAt: z11.string().datetime(),
11015
+ workspace: z11.string().min(1),
11016
+ profile: z11.string().nullable(),
11017
+ health: statusHealthSchema,
11018
+ repository: z11.object({
11019
+ available: z11.boolean(),
11020
+ branch: z11.string().nullable(),
11021
+ head: z11.string().nullable(),
11022
+ upstream: z11.string().nullable(),
11023
+ ahead: z11.number().int().nonnegative(),
11024
+ behind: z11.number().int().nonnegative(),
11025
+ dirtyCount: z11.number().int().nonnegative(),
11026
+ error: z11.string().optional()
11027
+ }),
11028
+ targets: z11.array(statusTargetSchema),
11029
+ members: z11.array(z11.object({
11030
+ id: z11.string().min(1),
11031
+ transport: z11.enum(["local", "ssh"]),
11032
+ workspace: z11.string().min(1),
11033
+ profile: z11.string().min(1),
11034
+ health: statusHealthSchema,
11035
+ agentwheelVersion: z11.string().nullable(),
11036
+ checkedAt: z11.string().nullable(),
11037
+ stale: z11.boolean(),
11038
+ error: z11.string().optional(),
11039
+ report: statusReportSchema.optional()
11040
+ }))
11041
+ }));
11042
+ var HEALTH_RANK = {
11043
+ PASS: 0,
11044
+ WARN: 1,
11045
+ STALE: 2,
11046
+ DEGRADED: 3,
11047
+ BUSY: 4,
11048
+ INCOMPATIBLE: 5,
11049
+ FAIL: 6
11050
+ };
11051
+ function worstStatusHealth(values) {
11052
+ return values.reduce(
11053
+ (worst, value) => HEALTH_RANK[value] > HEALTH_RANK[worst] ? value : worst,
11054
+ "PASS"
11055
+ );
11056
+ }
11057
+ function blocksCompositeApply(health) {
11058
+ return !["PASS", "WARN"].includes(health);
11059
+ }
11060
+
11061
+ // src/profile/members.ts
11062
+ var execFileAsync6 = promisify6(execFile6);
11063
+ var memberCacheSchema = z12.object({
11064
+ schemaVersion: z12.literal(1),
11065
+ checkedAt: z12.string().datetime(),
11066
+ report: statusReportSchema
11067
+ });
11068
+ async function collectCompositeMembers(options) {
11069
+ const chain = [...options.chain ?? [], compositeKey(options.workspaceRoot, options.profileName)];
11070
+ const results = [];
11071
+ for (const member of options.members) {
11072
+ results.push(await collectMember(member, options, chain));
11073
+ }
11074
+ return results;
11075
+ }
11076
+ async function collectMember(member, options, chain) {
11077
+ const cachePath = memberCachePath(options.workspaceRoot, options.profileName, member.id);
11078
+ const cached = await readMemberCache(cachePath);
11079
+ const ttlSeconds = member.refreshTtlSeconds ?? options.profileTtlSeconds;
11080
+ const ageMs = cached ? Date.now() - new Date(cached.checkedAt).getTime() : Number.POSITIVE_INFINITY;
11081
+ const fresh = ageMs <= ttlSeconds * 1e3;
11082
+ if (options.offline || cached && fresh && !options.refresh) {
11083
+ if (!cached) {
11084
+ return memberFailure(member, "STALE", "No cached member status is available offline.");
11085
+ }
11086
+ return memberFromReport(member, cached.report, {
11087
+ checkedAt: cached.checkedAt,
11088
+ stale: options.offline || !fresh,
11089
+ health: options.offline || !fresh ? worstStatusHealth([cached.report.health, "STALE"]) : cached.report.health
11090
+ });
11091
+ }
11092
+ try {
11093
+ const report = await invokeMemberStatus(member, options.workspaceRoot, chain, {
11094
+ refresh: options.refresh || !fresh,
11095
+ offline: false
11096
+ }, options.cliEntry);
11097
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
11098
+ await writeJsonAtomic(cachePath, { schemaVersion: 1, checkedAt, report });
11099
+ const versionHealth = report.agentwheelVersion === options.cliVersion ? "PASS" : "WARN";
11100
+ return memberFromReport(member, report, {
11101
+ checkedAt,
11102
+ stale: false,
11103
+ health: worstStatusHealth([report.health, versionHealth])
11104
+ });
11105
+ } catch (error) {
11106
+ const message = error instanceof Error ? error.message : String(error);
11107
+ const incompatible = /Incompatible member status protocol|Unknown option.*--json|Unknown command.*status/i.test(message);
11108
+ if (cached) {
11109
+ return memberFromReport(member, cached.report, {
11110
+ checkedAt: cached.checkedAt,
11111
+ stale: true,
11112
+ health: incompatible ? "INCOMPATIBLE" : "DEGRADED",
11113
+ error: message
11114
+ });
11115
+ }
11116
+ return memberFailure(member, incompatible ? "INCOMPATIBLE" : "FAIL", message);
11117
+ }
11118
+ }
11119
+ async function invokeMemberStatus(member, parentWorkspace, chain, options, cliEntry = process.argv[1]) {
11120
+ const args = ["--no-update-check", "status", "--profile", member.profile, "--json"];
11121
+ if (options.refresh) args.push("--refresh");
11122
+ if (options.offline) args.push("--offline");
11123
+ const env = { ...process.env, AGENTWHEEL_COMPOSITE_CHAIN: JSON.stringify(chain) };
11124
+ let stdout = "";
11125
+ let stderr = "";
11126
+ try {
11127
+ if (member.transport === "local") {
11128
+ const workspace = resolve21(parentWorkspace, member.workspace);
11129
+ const result = await execFileAsync6(process.execPath, [cliEntry, ...args], {
11130
+ cwd: workspace,
11131
+ env,
11132
+ maxBuffer: 20 * 1024 * 1024
11133
+ });
11134
+ stdout = result.stdout;
11135
+ stderr = result.stderr;
11136
+ } else {
11137
+ const sshArgs = sshArguments(member);
11138
+ const remoteArgs = [
11139
+ `cd ${shellQuote(member.workspace)}`,
11140
+ "&&",
11141
+ `AGENTWHEEL_COMPOSITE_CHAIN=${shellQuote(JSON.stringify(chain))}`,
11142
+ "agentwheel",
11143
+ ...args.map(shellQuote)
11144
+ ];
11145
+ const result = await execFileAsync6("ssh", [...sshArgs, remoteArgs.join(" ")], {
11146
+ env,
11147
+ maxBuffer: 20 * 1024 * 1024
11148
+ });
11149
+ stdout = result.stdout;
11150
+ stderr = result.stderr;
11151
+ }
11152
+ } catch (error) {
11153
+ if (typeof error === "object" && error !== null) {
11154
+ stdout = "stdout" in error ? String(error.stdout ?? "") : "";
11155
+ stderr = "stderr" in error ? String(error.stderr ?? "") : "";
11156
+ }
11157
+ if (!stdout.trim()) throw error;
11158
+ }
11159
+ try {
11160
+ return statusReportSchema.parse(JSON.parse(stdout));
11161
+ } catch (error) {
11162
+ const detail = stderr.trim() || (error instanceof Error ? error.message : String(error));
11163
+ throw new Error(`Incompatible member status protocol for ${member.id}: ${detail}`);
11164
+ }
11165
+ }
11166
+ async function runMemberAgentwheel(member, parentWorkspace, args, chain) {
11167
+ const env = { ...process.env, AGENTWHEEL_COMPOSITE_CHAIN: JSON.stringify(chain) };
11168
+ try {
11169
+ if (member.transport === "local") {
11170
+ const result2 = await execFileAsync6(
11171
+ process.execPath,
11172
+ [process.argv[1], "--no-update-check", ...args],
11173
+ {
11174
+ cwd: resolve21(parentWorkspace, member.workspace),
11175
+ env,
11176
+ maxBuffer: 20 * 1024 * 1024
11177
+ }
11178
+ );
11179
+ return { stdout: result2.stdout, stderr: result2.stderr };
11180
+ }
11181
+ const remoteArgs = [
11182
+ `cd ${shellQuote(member.workspace)}`,
11183
+ "&&",
11184
+ `AGENTWHEEL_COMPOSITE_CHAIN=${shellQuote(JSON.stringify(chain))}`,
11185
+ "agentwheel",
11186
+ "--no-update-check",
11187
+ ...args.map(shellQuote)
11188
+ ];
11189
+ const result = await execFileAsync6("ssh", [...sshArguments(member), remoteArgs.join(" ")], {
11190
+ env,
11191
+ maxBuffer: 20 * 1024 * 1024
11192
+ });
11193
+ return { stdout: result.stdout, stderr: result.stderr };
11194
+ } catch (error) {
11195
+ const detail = commandErrorDetail(error);
11196
+ if (/lock|busy|timed out waiting/i.test(detail)) {
11197
+ throw new Error(`BUSY ${member.id}: ${detail}`);
11198
+ }
11199
+ throw new Error(`Member ${member.id} command failed: ${detail}`);
11200
+ }
11201
+ }
11202
+ function sshArguments(member) {
11203
+ const destination = member.user ? `${member.user}@${member.host}` : member.host;
11204
+ return [
11205
+ ...member.port ? ["-p", String(member.port)] : [],
11206
+ ...member.identityFile ? ["-i", member.identityFile] : [],
11207
+ "--",
11208
+ destination
11209
+ ];
11210
+ }
11211
+ function shellQuote(value) {
11212
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
11213
+ }
11214
+ function commandErrorDetail(error) {
11215
+ if (typeof error === "object" && error !== null) {
11216
+ const stderr = "stderr" in error ? String(error.stderr).trim() : "";
11217
+ if (stderr) return stderr;
11218
+ }
11219
+ return error instanceof Error ? error.message : String(error);
11220
+ }
11221
+ function memberFromReport(member, report, state) {
11222
+ return {
11223
+ id: member.id,
11224
+ transport: member.transport,
11225
+ workspace: member.workspace,
11226
+ profile: member.profile,
11227
+ health: state.health,
11228
+ agentwheelVersion: report.agentwheelVersion,
11229
+ checkedAt: state.checkedAt,
11230
+ stale: state.stale,
11231
+ ...state.error ? { error: state.error } : {},
11232
+ report
11233
+ };
11234
+ }
11235
+ function memberFailure(member, health, error) {
11236
+ return {
11237
+ id: member.id,
11238
+ transport: member.transport,
11239
+ workspace: member.workspace,
11240
+ profile: member.profile,
11241
+ health,
11242
+ agentwheelVersion: null,
11243
+ checkedAt: null,
11244
+ stale: true,
11245
+ error
11246
+ };
11247
+ }
11248
+ function memberCachePath(workspaceRoot, profileName, memberId) {
11249
+ return join43(workspaceRoot, ".agentwheel", "cache", "member-status", profileName, `${memberId}.json`);
11250
+ }
11251
+ async function readMemberCache(path) {
11252
+ if (!await pathExists(path)) return void 0;
11253
+ try {
11254
+ return memberCacheSchema.parse(JSON.parse(await readFile31(path, "utf8")));
11255
+ } catch {
11256
+ return void 0;
11257
+ }
11258
+ }
11259
+ function parseCompositeChain() {
11260
+ const value = process.env.AGENTWHEEL_COMPOSITE_CHAIN;
11261
+ if (!value) return [];
11262
+ try {
11263
+ return z12.array(z12.string()).parse(JSON.parse(value));
11264
+ } catch {
11265
+ throw new Error("Invalid AGENTWHEEL_COMPOSITE_CHAIN protocol value.");
11266
+ }
11267
+ }
11268
+ function assertNoCompositeCycle(workspaceRoot, profileName, chain) {
11269
+ const key = compositeKey(workspaceRoot, profileName);
11270
+ if (chain.includes(key)) {
11271
+ throw new Error(`Composite profile cycle detected: ${[...chain, key].join(" -> ")}`);
11272
+ }
11273
+ }
11274
+ function compositeKey(workspaceRoot, profileName) {
11275
+ return `${resolve21(workspaceRoot)}#${profileName}`;
11276
+ }
11277
+
11278
+ // src/status/repository.ts
11279
+ import { execFile as execFile7 } from "child_process";
11280
+ import { promisify as promisify7 } from "util";
11281
+ var execFileAsync7 = promisify7(execFile7);
11282
+ async function collectRepositoryStatus(workspaceRoot) {
11283
+ try {
11284
+ const { stdout } = await execFileAsync7(
11285
+ "git",
11286
+ ["-C", workspaceRoot, "status", "--porcelain=v2", "--branch"],
11287
+ { maxBuffer: 10 * 1024 * 1024 }
11288
+ );
11289
+ const lines = stdout.split(/\r?\n/).filter(Boolean);
11290
+ const branch = valueAfter(lines, "# branch.head ");
11291
+ const head = valueAfter(lines, "# branch.oid ");
11292
+ const upstream = valueAfter(lines, "# branch.upstream ");
11293
+ const ab = valueAfter(lines, "# branch.ab ");
11294
+ const match = ab ? /^\+(\d+)\s+-(\d+)$/.exec(ab) : void 0;
11295
+ return {
11296
+ available: true,
11297
+ branch: branch === "(detached)" ? null : branch,
11298
+ head: head === "(initial)" ? null : head,
11299
+ upstream,
11300
+ ahead: match ? Number(match[1]) : 0,
11301
+ behind: match ? Number(match[2]) : 0,
11302
+ dirtyCount: lines.filter((line) => !line.startsWith("# ")).length
11303
+ };
11304
+ } catch (error) {
11305
+ return {
11306
+ available: false,
11307
+ branch: null,
11308
+ head: null,
11309
+ upstream: null,
11310
+ ahead: 0,
11311
+ behind: 0,
11312
+ dirtyCount: 0,
11313
+ error: error instanceof Error ? error.message : String(error)
11314
+ };
11315
+ }
11316
+ }
11317
+ function valueAfter(lines, prefix) {
11318
+ return lines.find((line) => line.startsWith(prefix))?.slice(prefix.length).trim() ?? null;
11319
+ }
11320
+
10605
11321
  // src/cli/index.ts
10606
11322
  var CLI_VERSION = resolveCliVersion();
10607
11323
  var COMPANION_SKILL_SOURCE = "github:NestDevLab/agentwheel";
@@ -10633,7 +11349,7 @@ program.command("init").description("initialize an agentwheel workspace or packa
10633
11349
  if (bootstrapPackage) console.log("Auto-added the agentwheel bootstrap skill for openclaw.");
10634
11350
  console.log(nextInstallNudge());
10635
11351
  });
10636
- program.command("add").description("add a package to .agentwheel/config.json without touching runtimes").argument("<source>", "package source").option("--driver <driver>", "source driver (local, git, skillkit, vercel-skills, mcp-registry, or clawhub)").option("--adapter <adapter>", "built-in adapter", "openclaw").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "workspace root").option("--mode <mode>", "pinned or tracking", "pinned").option("--name <name>", "package alias").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots on future installs", false).option("--suggestion <alias>", "include one suggested companion alias on future installs (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "allow this package to replace a colliding artifact (repeatable)", collectOverrideOption, []).action(async (source, options) => {
11352
+ program.command("add").description("add a package to .agentwheel/config.json without touching runtimes").argument("<source>", "package source").option("--driver <driver>", "source driver (local, git, skillkit, vercel-skills, mcp-registry, or clawhub)").option("--adapter <adapter>", "built-in adapter", "openclaw").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "workspace root").option("--mode <mode>", "pinned or tracking", "pinned").option("--version <range>", "root package version policy (exact, ~, ^, or *)").option("--name <name>", "package alias").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots on future installs", false).option("--suggestion <alias>", "include one suggested companion alias on future installs (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "allow this package to replace a colliding artifact (repeatable)", collectOverrideOption, []).action(async (source, options) => {
10637
11353
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
10638
11354
  const targetRoot = normalizeTargetRoot(normalizedOptions.targetRoot ?? process.cwd());
10639
11355
  const entry = await packageEntryFromSource(source, targetRoot, normalizedOptions);
@@ -10645,7 +11361,7 @@ program.command("list").description("list artifacts exposed by a package source"
10645
11361
  const resolvedInput = await resolvePackageSource(source, targetRoot);
10646
11362
  const selectedArtifacts = selectedArtifactsFromOptionsOrRegistry(options, resolvedInput.registryEntry);
10647
11363
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
10648
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join41(targetRoot, ".agentwheel", "cache") }))));
11364
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join44(targetRoot, ".agentwheel", "cache") }))));
10649
11365
  const artifacts = filterArtifactsBySelection(await driver.list(resolved), selectedArtifacts);
10650
11366
  for (const artifact of artifacts) {
10651
11367
  console.log(`${artifact.type} ${artifact.name} ${artifact.relativePath}`);
@@ -10655,7 +11371,7 @@ program.command("scan").description("scan a package source for validation findin
10655
11371
  const targetRoot = normalizeTargetRoot(options.targetRoot);
10656
11372
  const resolvedInput = await resolvePackageSource(source, targetRoot);
10657
11373
  const driver = getSourceDriver(options.driver ?? inferSourceDriverName(resolvedInput.source));
10658
- const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join41(targetRoot, ".agentwheel", "cache") }))));
11374
+ const resolved = await driver.export(await driver.translate(await driver.fetch(await driver.resolve(resolvedInput.source, { cacheRoot: join44(targetRoot, ".agentwheel", "cache") }))));
10659
11375
  const result = await driver.scan(resolved);
10660
11376
  if (result.findings.length === 0) {
10661
11377
  console.log("Scan ok: no findings");
@@ -10666,13 +11382,13 @@ program.command("scan").description("scan a package source for validation findin
10666
11382
  }
10667
11383
  if (!result.ok) process.exitCode = 1;
10668
11384
  });
10669
- program.command("plan").description("preview what install would reconcile without writing").argument("[name-or-source]", "configured package name/source or package source to preview").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--profile <name>", "workspace runtime profile").option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "for source previews, allow the source to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--dry-run", "accepted for symmetry; plan never writes", false).option("--format <fmt>", "output format: human|json|mermaid|html", "human").option("--json", "print the resolved plan as JSON", false).option("--force-drift", "replace drifted managed artifacts during install planning", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (source, options) => {
11385
+ program.command("plan").description("preview what install would reconcile without writing").argument("[name-or-source]", "configured package name/source or package source to preview").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--profile <name>", "workspace runtime profile").option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "for source previews, allow the source to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--dry-run", "accepted for symmetry; plan never writes", false).option("--format <fmt>", "output format: human|json|mermaid|html", "human").option("--json", "print the resolved plan as JSON", false).option("--force-drift", "replace drifted managed artifacts during install planning", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--refresh", "refresh available package versions even when the version-index TTL is fresh", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (source, options) => {
10670
11386
  await runInstallCommand(source, { ...options, dryRun: true }, { apply: false });
10671
11387
  });
10672
- program.command("install").description("install configured packages into runtime targets").argument("[name-or-source]", "configured package name/source or package source to add and install").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "when adding a source, allow it to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--format <fmt>", "output format: human|json|mermaid|html", "human").option("--json", "print the resolved plan as JSON", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).addHelpText("after", "\nScoped install never removes files owned only by other configured packages; run a full install to reconcile those removals.\n").action(async (source, options) => {
11388
+ program.command("install").description("install configured packages into runtime targets").argument("[name-or-source]", "configured package name/source or package source to add and install").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "when adding a source, allow it to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--format <fmt>", "output format: human|json|mermaid|html", "human").option("--json", "print the resolved plan as JSON", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--refresh", "refresh available package versions even when the version-index TTL is fresh", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).addHelpText("after", "\nScoped install never removes files owned only by other configured packages; run a full install to reconcile those removals.\n").action(async (source, options) => {
10673
11389
  await runInstallCommand(source, options, { apply: !options.dryRun });
10674
11390
  });
10675
- program.command("serve").description("serve a read-only live dashboard for the resolved install plan").argument("[name-or-source]", "configured package name/source or package source to preview").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--profile <name>", "workspace runtime profile").option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "for source previews, allow the source to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--force-drift", "replace drifted managed artifacts during install planning", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).option("--bind <addr>", "interface to bind", "127.0.0.1").option("--port <n>", "TCP port (0 selects an ephemeral port)", "8765").option("--interval <seconds>", "background re-render cadence in seconds", "60").option("--once", "render once and skip the background re-render loop", false).action(async (source, options) => {
11391
+ program.command("serve").description("serve a read-only live dashboard for the resolved install plan").argument("[name-or-source]", "configured package name/source or package source to preview").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--profile <name>", "workspace runtime profile").option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "for source previews, allow the source to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--force-drift", "replace drifted managed artifacts during install planning", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--refresh", "refresh available package versions even when the version-index TTL is fresh", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).option("--bind <addr>", "interface to bind", "127.0.0.1").option("--port <n>", "TCP port (0 selects an ephemeral port)", "8765").option("--interval <seconds>", "background re-render cadence in seconds", "60").option("--once", "render once and skip the background re-render loop", false).action(async (source, options) => {
10676
11392
  await servePlanDashboard({
10677
11393
  bind: options.bind,
10678
11394
  port: parseServePort(options.port),
@@ -10681,11 +11397,11 @@ program.command("serve").description("serve a read-only live dashboard for the r
10681
11397
  buildReport: () => buildPlanReport(source, options)
10682
11398
  });
10683
11399
  });
10684
- program.command("sync", { hidden: true }).argument("[name-or-source]", "configured package name/source or package source").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "when adding a source, allow it to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (source, options) => {
11400
+ program.command("sync", { hidden: true }).argument("[name-or-source]", "configured package name/source or package source").option("--driver <driver>", "source driver").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--all-detected", "run for every runtime directory detected in the target root", false).option("--mode <mode>", "pinned or tracking").option("--select <type/name>", "select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--override <source-or-package::type/name>", "when adding a source, allow it to replace a colliding artifact (repeatable)", collectOverrideOption, []).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plan without writing", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--only-source", "with a source argument, exclude configured workspace packages", false).option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--refresh", "refresh available package versions even when the version-index TTL is fresh", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (source, options) => {
10685
11401
  console.error("warning: 'agentwheel sync' is deprecated and will be removed in 0.10. Use 'agentwheel install'.");
10686
11402
  await runInstallCommand(source, options, { apply: !options.dryRun });
10687
11403
  });
10688
- program.command("update").description("re-resolve tracking packages, then apply the result").argument("[name]", "configured package name or source to update").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("-t, --target-root <path>", "workspace root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plans without writing", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--allow-adapter-code", "allow loading local adapter code from configured packages", false).option("--select <type/name>", "temporarily select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "temporarily select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--dependency <name-or-source>", "update one tracking dependency while keeping unrelated graph nodes locked (repeatable)", collectDependencyOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (name, options) => {
11404
+ program.command("update").description("re-resolve tracking packages, then apply the result").argument("[name]", "configured package name or source to update").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("-t, --target-root <path>", "workspace root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--profile <name>", "workspace runtime profile").option("--dry-run", "show plans without writing", false).option("--force-drift", "replace drifted managed artifacts", false).option("--force-conflict", "adopt unmanaged destinations when their content already matches the desired artifact", false).option("--replace-conflict", "replace unmanaged destinations even when their content differs", false).option("--execute-plugins", "execute semantic plugin installs", false).option("--reload-runtimes", "run configured runtime reload commands after executed semantic plugin changes", false).option("--restart-runtimes", "alias for --reload-runtimes", false).option("--allow-adapter-code", "allow loading local adapter code from configured packages", false).option("--select <type/name>", "temporarily select an artifact by type/name (repeatable or comma-separated)", collectSelectOption, []).option("--skill <name>", "temporarily select a skill by name (repeatable or comma-separated)", collectSkillOption, []).option("--dependency <name-or-source>", "update one tracking dependency while keeping unrelated graph nodes locked (repeatable)", collectDependencyOption, []).option("--with-suggestions", "include suggested companion artifacts for selected roots", false).option("--suggestion <alias>", "include one suggested companion alias (repeatable or comma-separated)", collectSuggestionOption, []).option("--no-deps", "resolve only root sources and ignore requires with a warning").option("--frozen-lock", "resolve strictly from the existing graph lock and cached sources", false).option("--offline", "resolve strictly from graph locks and local caches", false).option("--refresh", "refresh available package versions even when the version-index TTL is fresh", false).option("--yes", "trust all new transitive sources", false).option("--trust <pattern>", "pre-approve a transitive source glob (repeatable)", collectTrustOption, []).action(async (name, options) => {
10689
11405
  if (name && options.dependency.length > 0) throw new Error("A package argument cannot be combined with --dependency.");
10690
11406
  if (options.dependency.length > 0 && (options.select.length > 0 || options.skill.length > 0)) {
10691
11407
  throw new Error("--dependency cannot be combined with --select or --skill; package selections remain unchanged.");
@@ -10694,6 +11410,11 @@ program.command("update").description("re-resolve tracking packages, then apply
10694
11410
  throw new Error("--dependency cannot be combined with --frozen-lock or --offline.");
10695
11411
  }
10696
11412
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
11413
+ const composite = await resolveSelectedCompositeProfile(normalizedOptions);
11414
+ if (composite) {
11415
+ await runCompositeUpdate(composite.workspaceRoot, composite.name, composite.profile, name, normalizedOptions);
11416
+ return;
11417
+ }
10697
11418
  const targets = await resolveCliTargets(normalizedOptions, { preferAllProfile: true });
10698
11419
  for (const target of targets) {
10699
11420
  await runConfiguredGraphPackages(target, { ...normalizedOptions, scope: name }, { mode: "update" });
@@ -10889,9 +11610,25 @@ program.command("uninstall").description("remove configured packages or managed
10889
11610
  if (plan.hasBlockingChanges) process.exitCode = 1;
10890
11611
  }
10891
11612
  });
10892
- program.command("status").description("show configured packages and runtime install state").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--profile <name>", "workspace runtime profile").action(async (options) => {
11613
+ program.command("status").description("show configured packages and runtime install state").option("--adapter <adapter>", "built-in adapter or comma-separated adapters").option("-i, --installation-type <type>", "installation type (for example local or user)").option("--user", "shortcut for --installation-type user and home-scoped state", false).option("--local", "shortcut for --installation-type local", false).option("--adapter-config <path>", "adapter JSON/JSONC file").option("--adapter-module <path>", "local programmatic adapter module").option("--allow-adapter-code", "allow loading local adapter code", false).option("-t, --target-root <path>", "runtime/project root").option("--agent <name>", "named agent from merged config").option("--all", "run for every configured agent", false).option("--profile <name>", "workspace runtime profile").option("--json", "print the versioned status protocol as JSON", false).option("--offline", "use cached version and member status even when stale", false).option("--refresh", "refresh package and member status regardless of TTL", false).action(async (options) => {
10893
11614
  const normalizedOptions = normalizeRuntimeScopeOptions(options);
11615
+ const composite = await resolveSelectedCompositeProfile(normalizedOptions);
11616
+ if (composite) {
11617
+ const report = await collectCompositeStatus(composite.workspaceRoot, composite.name, composite.profile, normalizedOptions);
11618
+ if (options.json) console.log(JSON.stringify(report, null, 2));
11619
+ else printStatusReport(report);
11620
+ if (!["PASS", "WARN"].includes(report.health)) process.exitCode = 1;
11621
+ return;
11622
+ }
10894
11623
  const targets = await resolveCliTargets(normalizedOptions, { preferAllProfile: true });
11624
+ if (options.json) {
11625
+ const targetReports = [];
11626
+ for (const target of targets) targetReports.push(await collectTargetStatus(target, normalizedOptions));
11627
+ const report = await statusReport(targets[0]?.workspaceRoot ?? process.cwd(), options.profile ?? null, targetReports);
11628
+ console.log(JSON.stringify(report, null, 2));
11629
+ if (!["PASS", "WARN"].includes(report.health)) process.exitCode = 1;
11630
+ return;
11631
+ }
10895
11632
  for (const target of targets) {
10896
11633
  await printStatus(target, normalizedOptions);
10897
11634
  }
@@ -10907,7 +11644,7 @@ journalCommand.command("list").description("show pending apply journals for reso
10907
11644
  if (!journal) continue;
10908
11645
  pending += 1;
10909
11646
  console.log(`PENDING ${state.adapter.name}/${state.installationType} at ${state.installRoot}`);
10910
- console.log(` journal: ${join41(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
11647
+ console.log(` journal: ${join44(state.installRoot, ".agentwheel", `${state.state.stateKey}.apply-journal.json`)}`);
10911
11648
  console.log(` stateKey: ${state.state.stateKey}`);
10912
11649
  console.log(` createdAt: ${journal.createdAt}`);
10913
11650
  console.log(` updatedAt: ${journal.updatedAt}`);
@@ -10941,6 +11678,21 @@ program.command("doctor").description("check agentwheel runtime setup and compan
10941
11678
  async function runInstallCommand(nameOrSource, options, behavior) {
10942
11679
  const normalizedOptions = normalizeRuntimeScopeOptions(options, { defaultUser: shouldDefaultUserInstall(nameOrSource, options) });
10943
11680
  const outputFormat = effectivePlanOutputFormat(normalizedOptions);
11681
+ const composite = await resolveSelectedCompositeProfile(normalizedOptions);
11682
+ if (composite) {
11683
+ if (outputFormat !== "human") {
11684
+ throw new Error("Composite profile plan/install currently requires human output; use status --json for the versioned member protocol.");
11685
+ }
11686
+ await runCompositeInstall(
11687
+ composite.workspaceRoot,
11688
+ composite.name,
11689
+ composite.profile,
11690
+ nameOrSource,
11691
+ normalizedOptions,
11692
+ behavior
11693
+ );
11694
+ return;
11695
+ }
10944
11696
  if (outputFormat !== "human") {
10945
11697
  const report = await buildPlanReport(nameOrSource, normalizedOptions);
10946
11698
  if (report.targets.some((target) => target.hasBlockingChanges)) process.exitCode = 1;
@@ -11160,11 +11912,32 @@ async function packageEntryFromSource(source, targetRoot, options) {
11160
11912
  baseDir: targetRoot,
11161
11913
  warn: options.warn ?? ((message) => console.warn(message))
11162
11914
  });
11915
+ const provisionalName = options.name ?? resolvedInput.registryEntry?.name ?? source;
11916
+ const initialVersion = options.mode === "tracking" && options.version ? await effectiveTrackingRef({
11917
+ name: provisionalName,
11918
+ source: resolvedSource,
11919
+ driver: driverName,
11920
+ adapter: adapter.name,
11921
+ installationType: options.installationType,
11922
+ mode: "tracking",
11923
+ version: options.version
11924
+ }, targetRoot, { offline: options.offline }) : void 0;
11925
+ if (initialVersion?.availability?.error || initialVersion?.availability?.stale) {
11926
+ throw new Error(
11927
+ `Cannot select an initial version for ${provisionalName}: ${initialVersion.availability?.error ?? "version metadata is stale"}`
11928
+ );
11929
+ }
11930
+ if (initialVersion?.availability && !initialVersion.availability.latestAllowedRef) {
11931
+ throw new Error(
11932
+ `No available version of ${provisionalName} satisfies ${options.version}; latest overall is ${initialVersion.availability.latestOverall ?? "unknown"}.`
11933
+ );
11934
+ }
11163
11935
  const bundle = await stageSource(driver, resolvedSource, {
11164
11936
  workspaceRoot: targetRoot,
11165
11937
  adapter,
11166
- cacheRoot: join41(targetRoot, ".agentwheel", "cache"),
11938
+ cacheRoot: join44(targetRoot, ".agentwheel", "cache"),
11167
11939
  mode: options.mode,
11940
+ ref: initialVersion?.ref,
11168
11941
  frozenLock: lockMode,
11169
11942
  select: selectedArtifacts
11170
11943
  });
@@ -11180,6 +11953,7 @@ async function packageEntryFromSource(source, targetRoot, options) {
11180
11953
  adapterModule: options.adapterModule,
11181
11954
  adapterCodeHash: adapter.programmatic?.hash,
11182
11955
  mode: options.mode ?? "pinned",
11956
+ version: options.version,
11183
11957
  requestedRef: bundle.source.requestedRef,
11184
11958
  select: selectedArtifacts,
11185
11959
  withSuggestions: options.withSuggestions === true ? true : void 0,
@@ -11396,28 +12170,61 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
11396
12170
  const allPackages = [...group.packages, ...group.extraPackages];
11397
12171
  const groupHasScope = !scopedRootId || allPackages.some((pkg) => pkg.name === scopedRootId || pkg.source === targetOptions.scope);
11398
12172
  if (behavior.mode === "install" && scopedRootId && !groupHasScope) continue;
12173
+ const groupGraphLockPath = graphLockPathForTarget(
12174
+ group.target.workspaceRoot,
12175
+ targetKeyForTarget(group.target, adapter.name),
12176
+ adapter.name,
12177
+ targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
12178
+ );
12179
+ const previousGroupLock = await (await pathExists(groupGraphLockPath) ? readGraphLock(groupGraphLockPath) : void 0);
11399
12180
  const dependencyUpdateRootNames = /* @__PURE__ */ new Set();
11400
12181
  if (scopedDependencyUpdate) {
11401
- const graphLockPath = graphLockPathForTarget(
11402
- group.target.workspaceRoot,
11403
- targetKeyForTarget(group.target, adapter.name),
11404
- adapter.name,
11405
- targetFingerprintParts(group.target, adapter, group.adapterOptions, group.installationType)
11406
- );
11407
- const previousLock = await (await pathExists(graphLockPath) ? readGraphLock(graphLockPath) : void 0);
11408
12182
  for (const pkg of group.packages) {
11409
- const root = previousLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
12183
+ const root = previousGroupLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
11410
12184
  if (root?.mode !== "tracking") continue;
11411
- const node = previousLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId);
12185
+ const node = previousGroupLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId);
11412
12186
  if (node && dependencyUpdateSelectors.some((selector) => dependencyUpdateSelectorMatchesRoot(root, node, selector))) {
11413
12187
  dependencyUpdateRootNames.add(pkg.name);
11414
12188
  }
11415
12189
  }
11416
12190
  }
11417
12191
  const updateScope = behavior.mode === "update" ? scopedPackage ? /* @__PURE__ */ new Set([scopedPackage.name]) : void 0 : void 0;
12192
+ const versionSelections = /* @__PURE__ */ new Map();
12193
+ const lockedVersionRefs = /* @__PURE__ */ new Map();
12194
+ const versionPolicyUpdateNames = /* @__PURE__ */ new Set();
12195
+ for (const pkg of allPackages) {
12196
+ if (pkg.mode !== "tracking" || !pkg.version) continue;
12197
+ const previousRoot = previousGroupLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
12198
+ const previousNode = previousRoot ? previousGroupLock?.canonical.nodes.find((candidate) => candidate.id === previousRoot.graphNodeId) : void 0;
12199
+ const policyRequiresResolution = !previousNode || !satisfiesVersionRange(previousNode.version, pkg.version);
12200
+ if (!policyRequiresResolution && previousNode?.requestedRef) {
12201
+ lockedVersionRefs.set(pkg.name, previousNode.requestedRef);
12202
+ }
12203
+ const packageUpdateSelected = !updateScope || updateScope.has(pkg.name);
12204
+ if (behavior.mode === "update" && packageUpdateSelected || behavior.mode === "install" && policyRequiresResolution) {
12205
+ const selection = await effectiveTrackingRef(pkg, group.target.workspaceRoot, {
12206
+ offline: targetOptions.offline,
12207
+ forceRefresh: targetOptions.refresh
12208
+ });
12209
+ if (selection.availability?.error || selection.availability?.stale) {
12210
+ throw new Error(
12211
+ `Cannot resolve ${pkg.name} with stale version metadata: ${selection.availability?.error ?? "version index TTL expired"}`
12212
+ );
12213
+ }
12214
+ versionSelections.set(pkg.name, selection);
12215
+ if (policyRequiresResolution) versionPolicyUpdateNames.add(pkg.name);
12216
+ if (!selection.availability?.latestAllowed) {
12217
+ targetOptions.warn?.(
12218
+ `No available version of ${pkg.name} satisfies ${pkg.version}; latest overall is ${selection.availability?.latestOverall ?? "unknown"}.`
12219
+ );
12220
+ }
12221
+ }
12222
+ }
11418
12223
  const roots = [
11419
12224
  ...allPackages.map((pkg) => {
11420
- const updateThisPackage = behavior.mode === "update" && !scopedDependencyUpdate && pkg.mode === "tracking" && (!updateScope || updateScope.has(pkg.name) || updateScope.has(pkg.source));
12225
+ const versionSelection = versionSelections.get(pkg.name);
12226
+ const versionAllowsUpdate = !pkg.version || Boolean(versionSelection?.availability?.latestAllowed);
12227
+ const updateThisPackage = behavior.mode === "update" && !scopedDependencyUpdate && pkg.mode === "tracking" && versionAllowsUpdate && (!updateScope || updateScope.has(pkg.name) || updateScope.has(pkg.source));
11421
12228
  const packageIsScoped = scopedRootId ? pkg.name === scopedRootId || pkg.source === targetOptions.scope : true;
11422
12229
  if (pkg.selection && selectedArtifacts && packageIsScoped) {
11423
12230
  throw new Error(`--select/--skill cannot override imported selection for configured package '${pkg.name}'.`);
@@ -11426,14 +12233,15 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
11426
12233
  rootId: pkg.name,
11427
12234
  source: pkg.source,
11428
12235
  mode: pkg.mode,
11429
- ref: pkg.requestedRef,
12236
+ version: pkg.version,
12237
+ ref: versionSelection?.ref ?? lockedVersionRefs.get(pkg.name) ?? pkg.requestedRef,
11430
12238
  select: pkg.selection ? void 0 : selectedArtifacts && packageIsScoped ? selectedArtifacts : normalizeArtifactSelectors(pkg.select, pkg.skills),
11431
12239
  selection: pkg.selection,
11432
12240
  aliases: pkg.aliases,
11433
12241
  overrides: pkg.overrides,
11434
12242
  includeSuggestions: targetOptions.withSuggestions === true || pkg.withSuggestions === true,
11435
12243
  suggestionAliases: packageSuggestionAliases(pkg, targetOptions),
11436
- useLock: behavior.mode === "install" ? true : scopedDependencyUpdate ? !dependencyUpdateRootNames.has(pkg.name) : !updateThisPackage
12244
+ useLock: behavior.mode === "install" ? !versionPolicyUpdateNames.has(pkg.name) : scopedDependencyUpdate ? !dependencyUpdateRootNames.has(pkg.name) : !updateThisPackage
11437
12245
  };
11438
12246
  }),
11439
12247
  ...group.extraRoots
@@ -11474,7 +12282,7 @@ async function buildGraphPlansForTarget(target, source, options, behavior) {
11474
12282
  if ((behavior.mode === "install" || behavior.mode === "update") && scopedRootId) {
11475
12283
  const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
11476
12284
  const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
11477
- results.push(scopeInstallPlanToRoot(result, scopedRootId, manifest));
12285
+ results.push(behavior.mode === "update" && previousGroupLock ? scopeUpdatePlanToRoot(result, scopedRootId, previousGroupLock, manifest) : scopeInstallPlanToRoot(result, scopedRootId, manifest));
11478
12286
  } else if (scopedDependencyUpdate) {
11479
12287
  const state = installStateForTarget(group.target, adapter, group.adapterOptions, group.installationType);
11480
12288
  const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
@@ -11678,6 +12486,44 @@ function scopeInstallPlanToRoot(result, rootId, manifest) {
11678
12486
  }
11679
12487
  };
11680
12488
  }
12489
+ function scopeUpdatePlanToRoot(result, rootId, previousLock, manifest) {
12490
+ const scoped = scopeInstallPlanToRoot(result, rootId, manifest);
12491
+ const selectedCurrentNodeIds = graphRootClosure(scoped.bundle.graphLock, rootId);
12492
+ const selectedPreviousNodeIds = graphRootClosure(previousLock, rootId);
12493
+ const graphLock = preserveUnrelatedGraphPackages(
12494
+ scoped.bundle.graphLock,
12495
+ previousLock,
12496
+ selectedCurrentNodeIds,
12497
+ selectedPreviousNodeIds,
12498
+ /* @__PURE__ */ new Set([rootId])
12499
+ );
12500
+ const graphLockDigest = createHash11("sha256").update(canonicalGraphLockJson(graphLock)).digest("hex");
12501
+ return {
12502
+ ...scoped,
12503
+ bundle: { ...scoped.bundle, graphLock },
12504
+ graphLockDigest,
12505
+ graphDiff: diffGraphLocks(previousLock, graphLock),
12506
+ plan: {
12507
+ ...scoped.plan,
12508
+ graphLockDigest
12509
+ }
12510
+ };
12511
+ }
12512
+ function graphRootClosure(lock, rootId) {
12513
+ const root = lock.canonical.roots.find((candidate) => candidate.rootId === rootId);
12514
+ if (!root) return /* @__PURE__ */ new Set();
12515
+ const selected = /* @__PURE__ */ new Set();
12516
+ const queue = [root.graphNodeId];
12517
+ while (queue.length > 0) {
12518
+ const nodeId = queue.shift();
12519
+ if (selected.has(nodeId)) continue;
12520
+ selected.add(nodeId);
12521
+ for (const edge of lock.canonical.edges) {
12522
+ if (edge.from === nodeId) queue.push(edge.to);
12523
+ }
12524
+ }
12525
+ return selected;
12526
+ }
11681
12527
  function transformOutOfScopeOperation(operation, entry, targetRoot, scopeDescription) {
11682
12528
  if (operation.action === "skip") return [operation];
11683
12529
  if (operation.action === "update" || operation.action === "drift") {
@@ -11723,7 +12569,7 @@ function keepManifestEntryOperation(entry, targetRoot, scopeDescription, operati
11723
12569
  artifactType: entry.artifactType,
11724
12570
  artifactName: entry.artifactName,
11725
12571
  kind: entry.kind,
11726
- destPath: operation?.destPath ?? join41(targetRoot, entry.path),
12572
+ destPath: operation?.destPath ?? join44(targetRoot, entry.path),
11727
12573
  relativeDestPath: entry.path,
11728
12574
  desiredHash: entry.sourceHash,
11729
12575
  currentHash: operation?.currentHash ?? entry.hash,
@@ -11785,6 +12631,7 @@ async function uninstallConfiguredPackage(target, packageName, options) {
11785
12631
  rootId: pkg2.name,
11786
12632
  source: pkg2.source,
11787
12633
  mode: pkg2.mode,
12634
+ version: pkg2.version,
11788
12635
  ref: pkg2.requestedRef,
11789
12636
  select: pkg2.selection ? void 0 : normalizeArtifactSelectors(pkg2.select, pkg2.skills),
11790
12637
  selection: pkg2.selection,
@@ -11921,31 +12768,341 @@ async function readTargetGraphLock(target, options) {
11921
12768
  return { adapter, path, lock: await readGraphLock(path) };
11922
12769
  }
11923
12770
  async function printStatus(target, options) {
12771
+ const report = await collectTargetStatus(target, options);
12772
+ printTargetStatus(report);
12773
+ }
12774
+ async function collectTargetStatus(target, options) {
11924
12775
  const config = await readMergedWorkspaceConfig(target.workspaceRoot);
11925
12776
  const adapterOptions = adapterOptionsForTarget(target, options);
11926
- const adapter = await resolveAdapterForTarget(target, adapterOptions);
12777
+ const adapter = await resolveAdapterForTarget(target, { ...adapterOptions, warn: () => void 0 });
11927
12778
  const transport = transportForTarget(target);
11928
12779
  const installationType = options.installationType ?? target.installationType ?? resolveInstallationTypeForAdapter(adapter);
11929
12780
  const state = installStateForTarget(target, adapter, adapterOptions, installationType);
11930
- console.log(`Status for ${adapter.name}/${installationType} at ${state.installRoot}`);
11931
- if (config.packages.length === 0) {
11932
- console.log(`Configured packages: none at ${target.workspaceRoot}`);
11933
- return;
11934
- }
11935
- console.log("Configured packages:");
11936
- for (const pkg of config.packages) {
11937
- console.log(`- ${pkg.name} (${pkg.mode}) ${pkg.source}`);
11938
- }
11939
12781
  const manifest = await readInstallManifest(state.installRoot, adapter.name, transport, state);
11940
- console.log(manifest ? `Install manifest: ${manifest.entries.length} entries, revision ${manifest.revision}` : "Install manifest: missing");
12782
+ let graphLockPath = null;
12783
+ let graphLock;
11941
12784
  try {
11942
- const { path, lock } = await readTargetGraphLock(target, adapterOptions);
11943
- console.log(`Graph lock: ${path}`);
11944
- console.log(`Locked graph: ${lock.canonical.roots.length} roots, ${lock.canonical.nodes.length} nodes, ${lock.canonical.artifacts.length} artifacts`);
12785
+ const result = await readTargetGraphLock(target, adapterOptions);
12786
+ graphLockPath = result.path;
12787
+ graphLock = result.lock;
11945
12788
  } catch {
11946
- console.log("Graph lock: missing");
12789
+ graphLock = void 0;
12790
+ }
12791
+ const packages = [];
12792
+ for (const pkg of config.packages) {
12793
+ const root = graphLock?.canonical.roots.find((candidate) => candidate.rootId === pkg.name);
12794
+ const node = root ? graphLock?.canonical.nodes.find((candidate) => candidate.id === root.graphNodeId) : void 0;
12795
+ const availability = await discoverPackageVersions(pkg, target.workspaceRoot, {
12796
+ forceRefresh: options.refresh,
12797
+ offline: options.offline
12798
+ });
12799
+ const installed = node && manifest?.entries.some((entry) => "graphNodeId" in entry && entry.graphNodeId === node.id) ? node.version : null;
12800
+ const locked = node?.version ?? null;
12801
+ const baseline = installed ?? locked;
12802
+ packages.push({
12803
+ name: pkg.name,
12804
+ source: pkg.source,
12805
+ mode: pkg.mode,
12806
+ policy: pkg.version ?? "*",
12807
+ installed,
12808
+ locked,
12809
+ latestAllowed: availability.latestAllowed,
12810
+ latestOverall: availability.latestOverall,
12811
+ availability: availability.stale ? "STALE" : availability.checkedAt ? "FRESH" : "UNKNOWN",
12812
+ checkedAt: availability.checkedAt,
12813
+ ...availability.error ? { error: availability.error } : {},
12814
+ updateAvailableAllowed: Boolean(
12815
+ baseline && availability.latestAllowed && compareSemverStrings(availability.latestAllowed, baseline) > 0
12816
+ ),
12817
+ updateAvailableOverall: Boolean(
12818
+ baseline && availability.latestOverall && compareSemverStrings(availability.latestOverall, baseline) > 0
12819
+ )
12820
+ });
12821
+ }
12822
+ const pending = await collectPendingInstallWork(target, options);
12823
+ const artifacts = (graphLock?.canonical.artifacts ?? []).map((artifact) => {
12824
+ const node = graphLock?.canonical.nodes.find((candidate) => candidate.id === artifact.graphNodeId);
12825
+ const installed = manifest?.entries.some((entry) => {
12826
+ if (!("graphNodeId" in entry) || entry.graphNodeId !== artifact.graphNodeId) return false;
12827
+ if ("logicalSelector" in entry && entry.logicalSelector) return entry.logicalSelector === artifact.logicalSelector;
12828
+ return entry.artifactType === artifact.type && entry.artifactName === artifact.name;
12829
+ }) ?? false;
12830
+ return {
12831
+ selector: artifact.logicalSelector,
12832
+ type: artifact.type,
12833
+ name: artifact.name,
12834
+ installName: artifact.installName,
12835
+ packageName: node?.name ?? null,
12836
+ packageVersion: node?.version ?? null,
12837
+ hash: artifact.hash,
12838
+ installed
12839
+ };
12840
+ });
12841
+ const health = [];
12842
+ if (!manifest || !graphLock) health.push("FAIL");
12843
+ if (pending.error) health.push("DEGRADED");
12844
+ if (pending.driftCount > 0 || pending.conflictCount > 0) health.push("FAIL");
12845
+ else if (pending.pendingCount > 0) health.push("WARN");
12846
+ if (packages.some((pkg) => pkg.availability === "STALE" || pkg.error)) health.push("DEGRADED");
12847
+ else if (packages.some((pkg) => pkg.updateAvailableAllowed || pkg.updateAvailableOverall)) health.push("WARN");
12848
+ return {
12849
+ adapter: adapter.name,
12850
+ installationType,
12851
+ targetRoot: state.installRoot,
12852
+ health: worstStatusHealth(health),
12853
+ manifestRevision: manifest?.revision ?? null,
12854
+ manifestEntryCount: manifest?.entries.length ?? 0,
12855
+ graphLockPath,
12856
+ packageCount: packages.length,
12857
+ artifactCount: graphLock?.canonical.artifacts.length ?? 0,
12858
+ pendingCount: pending.pendingCount,
12859
+ driftCount: pending.driftCount,
12860
+ conflictCount: pending.conflictCount,
12861
+ ...pending.error ? { error: pending.error } : {},
12862
+ packages,
12863
+ artifacts
12864
+ };
12865
+ }
12866
+ async function resolveSelectedCompositeProfile(options) {
12867
+ const workspaceRoot = await findWorkspaceRoot(options.targetRoot ?? process.cwd());
12868
+ const config = await readMergedWorkspaceConfig(workspaceRoot);
12869
+ const name = options.profile ?? (options.all && config.profiles.all ? "all" : void 0);
12870
+ if (!name) return void 0;
12871
+ const profile = config.profiles[name];
12872
+ if (!profile) throw new Error(`Unknown profile: ${name}`);
12873
+ if (!isCompositeWorkspaceProfile(profile)) return void 0;
12874
+ const chain = parseCompositeChain();
12875
+ assertNoCompositeCycle(workspaceRoot, name, chain);
12876
+ return { workspaceRoot, name, profile };
12877
+ }
12878
+ async function collectCompositeStatus(workspaceRoot, profileName, profile, options) {
12879
+ const chain = parseCompositeChain();
12880
+ const members = await collectCompositeMembers({
12881
+ cliVersion: CLI_VERSION,
12882
+ workspaceRoot,
12883
+ profileName,
12884
+ profileTtlSeconds: profile.refreshTtlSeconds,
12885
+ members: profile.members,
12886
+ refresh: options.refresh,
12887
+ offline: options.offline,
12888
+ chain
12889
+ });
12890
+ const repository = await collectRepositoryStatus(workspaceRoot);
12891
+ const repositoryHealth = repository.available && repository.ahead === 0 && repository.behind === 0 && repository.dirtyCount === 0 ? "PASS" : "WARN";
12892
+ return {
12893
+ schemaVersion: 1,
12894
+ command: "status",
12895
+ agentwheelVersion: CLI_VERSION,
12896
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
12897
+ workspace: workspaceRoot,
12898
+ profile: profileName,
12899
+ health: worstStatusHealth([...members.map((member) => member.health), repositoryHealth]),
12900
+ repository,
12901
+ targets: [],
12902
+ members
12903
+ };
12904
+ }
12905
+ async function runCompositeUpdate(workspaceRoot, profileName, profile, packageName, options) {
12906
+ const preflight = await collectCompositeStatus(workspaceRoot, profileName, profile, options);
12907
+ printStatusReport(preflight);
12908
+ const blockers = preflight.members.filter((member) => blocksCompositeApply(member.health));
12909
+ if (blockers.length > 0) {
12910
+ throw new Error(
12911
+ `Composite update blocked before member execution: ` + blockers.map((member) => `${member.id}=${member.health}`).join(", ")
12912
+ );
12913
+ }
12914
+ const incomingChain = parseCompositeChain();
12915
+ const memberChain = [...incomingChain, compositeKey(workspaceRoot, profileName)];
12916
+ for (const member of profile.members) {
12917
+ if (!options.dryRun) {
12918
+ const before = preflight.members.find((candidate) => candidate.id === member.id);
12919
+ const revalidated = await collectCompositeMembers({
12920
+ cliVersion: CLI_VERSION,
12921
+ workspaceRoot,
12922
+ profileName,
12923
+ profileTtlSeconds: profile.refreshTtlSeconds,
12924
+ members: [member],
12925
+ refresh: true,
12926
+ chain: incomingChain
12927
+ });
12928
+ const current = revalidated[0];
12929
+ if (blocksCompositeApply(current.health)) {
12930
+ throw new Error(`Composite update stopped before ${member.id}: revalidation is ${current.health}.`);
12931
+ }
12932
+ if (statusRevisionSignature(before?.report) !== statusRevisionSignature(current.report)) {
12933
+ throw new Error(`Composite update stopped before ${member.id}: member revision changed after preflight.`);
12934
+ }
12935
+ }
12936
+ const args = compositeUpdateArguments(member.profile, packageName, options);
12937
+ console.log(`${options.dryRun ? "Plan" : "Update"} member ${member.id}:`);
12938
+ const result = await runMemberAgentwheel(member, workspaceRoot, args, memberChain);
12939
+ if (result.stdout.trim()) console.log(result.stdout.trimEnd());
12940
+ if (result.stderr.trim()) console.error(result.stderr.trimEnd());
11947
12941
  }
11948
- await printPendingInstallWork(target, options);
12942
+ }
12943
+ async function runCompositeInstall(workspaceRoot, profileName, profile, nameOrSource, options, behavior) {
12944
+ const preflight = await collectCompositeStatus(workspaceRoot, profileName, profile, options);
12945
+ printStatusReport(preflight);
12946
+ const blockers = preflight.members.filter((member) => blocksCompositeApply(member.health));
12947
+ if (blockers.length > 0) {
12948
+ throw new Error(
12949
+ `Composite ${behavior.apply ? "install" : "plan"} blocked before member execution: ` + blockers.map((member) => `${member.id}=${member.health}`).join(", ")
12950
+ );
12951
+ }
12952
+ const incomingChain = parseCompositeChain();
12953
+ const memberChain = [...incomingChain, compositeKey(workspaceRoot, profileName)];
12954
+ for (const member of profile.members) {
12955
+ if (behavior.apply) {
12956
+ const before = preflight.members.find((candidate) => candidate.id === member.id);
12957
+ const revalidated = await collectCompositeMembers({
12958
+ cliVersion: CLI_VERSION,
12959
+ workspaceRoot,
12960
+ profileName,
12961
+ profileTtlSeconds: profile.refreshTtlSeconds,
12962
+ members: [member],
12963
+ refresh: true,
12964
+ chain: incomingChain
12965
+ });
12966
+ const current = revalidated[0];
12967
+ if (blocksCompositeApply(current.health)) {
12968
+ throw new Error(`Composite install stopped before ${member.id}: revalidation is ${current.health}.`);
12969
+ }
12970
+ if (statusRevisionSignature(before?.report) !== statusRevisionSignature(current.report)) {
12971
+ throw new Error(`Composite install stopped before ${member.id}: member revision changed after preflight.`);
12972
+ }
12973
+ }
12974
+ const args = compositeInstallArguments(member.profile, nameOrSource, options, behavior.apply);
12975
+ console.log(`${behavior.apply ? "Install" : "Plan"} member ${member.id}:`);
12976
+ const result = await runMemberAgentwheel(member, workspaceRoot, args, memberChain);
12977
+ if (result.stdout.trim()) console.log(result.stdout.trimEnd());
12978
+ if (result.stderr.trim()) console.error(result.stderr.trimEnd());
12979
+ }
12980
+ }
12981
+ function compositeInstallArguments(profile, nameOrSource, options, apply) {
12982
+ const args = [apply ? "install" : "plan"];
12983
+ if (nameOrSource) args.push(nameOrSource);
12984
+ args.push("--profile", profile);
12985
+ if (options.refresh) args.push("--refresh");
12986
+ if (options.forceDrift) args.push("--force-drift");
12987
+ if (options.forceConflict) args.push("--force-conflict");
12988
+ if (options.replaceConflict) args.push("--replace-conflict");
12989
+ if (options.executePlugins) args.push("--execute-plugins");
12990
+ if (shouldReloadRuntimes(options)) args.push("--reload-runtimes");
12991
+ if (options.noDeps) args.push("--no-deps");
12992
+ if (options.frozenLock) args.push("--frozen-lock");
12993
+ if (options.onlySource) args.push("--only-source");
12994
+ for (const selection of options.select ?? []) args.push("--select", selection);
12995
+ for (const skill of options.skill ?? []) args.push("--skill", skill);
12996
+ for (const trust of options.trust ?? []) args.push("--trust", trust);
12997
+ if (options.yes) args.push("--yes");
12998
+ return args;
12999
+ }
13000
+ function compositeUpdateArguments(profile, packageName, options) {
13001
+ const args = ["update"];
13002
+ if (packageName) args.push(packageName);
13003
+ args.push("--profile", profile);
13004
+ if (options.dryRun) args.push("--dry-run");
13005
+ if (options.refresh) args.push("--refresh");
13006
+ if (options.forceDrift) args.push("--force-drift");
13007
+ if (options.forceConflict) args.push("--force-conflict");
13008
+ if (options.replaceConflict) args.push("--replace-conflict");
13009
+ if (options.executePlugins) args.push("--execute-plugins");
13010
+ if (shouldReloadRuntimes(options)) args.push("--reload-runtimes");
13011
+ if (options.noDeps) args.push("--no-deps");
13012
+ if (options.frozenLock) args.push("--frozen-lock");
13013
+ for (const dependency of options.dependency ?? []) args.push("--dependency", dependency);
13014
+ for (const trust of options.trust ?? []) args.push("--trust", trust);
13015
+ if (options.yes) args.push("--yes");
13016
+ return args;
13017
+ }
13018
+ function statusRevisionSignature(report) {
13019
+ if (!report) return "missing";
13020
+ return JSON.stringify({
13021
+ profile: report.profile,
13022
+ repository: {
13023
+ head: report.repository.head,
13024
+ ahead: report.repository.ahead,
13025
+ behind: report.repository.behind,
13026
+ dirtyCount: report.repository.dirtyCount
13027
+ },
13028
+ targets: report.targets.map((target) => ({
13029
+ adapter: target.adapter,
13030
+ installationType: target.installationType,
13031
+ targetRoot: target.targetRoot,
13032
+ manifestRevision: target.manifestRevision,
13033
+ graphLockPath: target.graphLockPath,
13034
+ packages: target.packages.map((pkg) => ({
13035
+ name: pkg.name,
13036
+ installed: pkg.installed,
13037
+ locked: pkg.locked,
13038
+ latestAllowed: pkg.latestAllowed,
13039
+ latestOverall: pkg.latestOverall
13040
+ }))
13041
+ })),
13042
+ members: report.members.map((member) => ({
13043
+ id: member.id,
13044
+ report: statusRevisionSignature(member.report)
13045
+ }))
13046
+ });
13047
+ }
13048
+ async function statusReport(workspace, profile, targets) {
13049
+ const repository = await collectRepositoryStatus(workspace);
13050
+ const repositoryHealth = repository.available && repository.ahead === 0 && repository.behind === 0 && repository.dirtyCount === 0 ? "PASS" : "WARN";
13051
+ return {
13052
+ schemaVersion: 1,
13053
+ command: "status",
13054
+ agentwheelVersion: CLI_VERSION,
13055
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13056
+ workspace,
13057
+ profile,
13058
+ health: worstStatusHealth([...targets.map((target) => target.health), repositoryHealth]),
13059
+ repository,
13060
+ targets,
13061
+ members: []
13062
+ };
13063
+ }
13064
+ function printStatusReport(report) {
13065
+ console.log(`Status ${report.health} for profile ${report.profile ?? "(direct)"} at ${report.workspace}`);
13066
+ console.log(
13067
+ `Repository: ${report.repository.available ? report.repository.branch ?? "detached" : "unavailable"}; ahead=${report.repository.ahead}; behind=${report.repository.behind}; dirty=${report.repository.dirtyCount}`
13068
+ );
13069
+ if (report.members.length > 0) {
13070
+ console.log("MEMBER TRANSPORT PROFILE VERSION HEALTH CACHE");
13071
+ for (const member of report.members) {
13072
+ console.log([
13073
+ member.id,
13074
+ member.transport,
13075
+ member.profile,
13076
+ member.agentwheelVersion ?? "unknown",
13077
+ member.health,
13078
+ member.stale ? "stale" : "fresh"
13079
+ ].join(" "));
13080
+ if (member.error) console.log(` ${member.error}`);
13081
+ }
13082
+ }
13083
+ for (const target of report.targets) printTargetStatus(target);
13084
+ }
13085
+ function printTargetStatus(target) {
13086
+ console.log(`Status for ${target.adapter}/${target.installationType} at ${target.targetRoot} (health: ${target.health})`);
13087
+ console.log(target.manifestRevision ? `Install manifest: ${target.manifestEntryCount} entries, revision ${target.manifestRevision}` : "Install manifest: missing");
13088
+ console.log(target.graphLockPath ? `Graph lock: ${target.graphLockPath} (${target.artifactCount} artifacts)` : "Graph lock: missing");
13089
+ console.log("PACKAGE MODE POLICY INSTALLED LOCKED LATEST ALLOWED LATEST OVERALL STATUS");
13090
+ for (const pkg of target.packages) {
13091
+ console.log([
13092
+ pkg.name,
13093
+ pkg.mode,
13094
+ pkg.policy,
13095
+ pkg.installed ?? "-",
13096
+ pkg.locked ?? "-",
13097
+ pkg.latestAllowed ?? "-",
13098
+ pkg.latestOverall ?? "-",
13099
+ pkg.availability
13100
+ ].join(" "));
13101
+ }
13102
+ console.log(`Artifacts: ${target.artifactCount} locked, ${target.artifacts.filter((artifact) => artifact.installed).length} installed`);
13103
+ if (target.error) console.log(`Pending install work: unavailable (${target.error})`);
13104
+ else if (target.pendingCount === 0) console.log("Pending install work: none");
13105
+ else console.log(`Pending install work: ${target.pendingCount} (drift=${target.driftCount}, conflict=${target.conflictCount})`);
11949
13106
  }
11950
13107
  async function journalStateForTarget(target, options) {
11951
13108
  const adapterOptions = adapterOptionsForTarget(target, options);
@@ -11955,25 +13112,30 @@ async function journalStateForTarget(target, options) {
11955
13112
  const state = installStateForTarget(target, adapter, adapterOptions, installationType);
11956
13113
  return { adapter, transport, installationType, installRoot: state.installRoot, state };
11957
13114
  }
11958
- async function printPendingInstallWork(target, options) {
13115
+ async function collectPendingInstallWork(target, options) {
11959
13116
  let results = [];
11960
13117
  try {
11961
- results = await buildGraphPlansForTarget(target, void 0, { ...options, dryRun: true }, { mode: "install" });
13118
+ results = await buildGraphPlansForTarget(
13119
+ target,
13120
+ void 0,
13121
+ { ...options, dryRun: true, warn: options.warn ?? (() => void 0) },
13122
+ { mode: "install" }
13123
+ );
11962
13124
  const operations = results.flatMap((result) => result.plan.operations);
11963
13125
  const pending = operations.filter(isPendingInstallOperation);
11964
- if (pending.length === 0) {
11965
- console.log("Pending install work: none");
11966
- return;
11967
- }
11968
- const counts = [...pending.reduce((map, operation) => {
13126
+ const counts = Object.fromEntries([...pending.reduce((map, operation) => {
11969
13127
  map.set(operation.action, (map.get(operation.action) ?? 0) + 1);
11970
13128
  return map;
11971
- }, /* @__PURE__ */ new Map())].map(([action, count]) => `${action}=${count}`).join(", ");
11972
- const blocking = pending.filter((operation) => operation.action === "conflict" || operation.action === "drift").length;
11973
- console.log(`Pending install work: ${pending.length} operations (${counts}${blocking ? `; blocking=${blocking}` : ""})`);
13129
+ }, /* @__PURE__ */ new Map())]);
13130
+ return {
13131
+ pendingCount: pending.length,
13132
+ driftCount: counts.drift ?? 0,
13133
+ conflictCount: counts.conflict ?? 0,
13134
+ counts
13135
+ };
11974
13136
  } catch (error) {
11975
13137
  const message = error instanceof Error ? error.message : String(error);
11976
- console.log(`Pending install work: unavailable (${message})`);
13138
+ return { pendingCount: 0, driftCount: 0, conflictCount: 0, counts: {}, error: message };
11977
13139
  } finally {
11978
13140
  await Promise.all(results.map((result) => rm11(result.bundle.root, { recursive: true, force: true })));
11979
13141
  }
@@ -11992,12 +13154,12 @@ async function printDoctor(target, options) {
11992
13154
  const requestedSkills = doctorSkillRequests(target, options);
11993
13155
  const skills = [];
11994
13156
  for (const request of requestedSkills) {
11995
- const skillPath = join41(state.installRoot, targetMapping.dest, request.name);
13157
+ const skillPath = join44(state.installRoot, targetMapping.dest, request.name);
11996
13158
  const exists = await pathExists(skillPath);
11997
13159
  const manifestEntry = manifest?.entries.find((entry) => {
11998
13160
  if (entry.artifactType !== "skills") return false;
11999
13161
  const legacyInstallName = "installName" in entry && typeof entry.installName === "string" ? entry.installName : void 0;
12000
- return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join41(targetMapping.dest, request.name);
13162
+ return entry.artifactName === request.name || legacyInstallName === request.name || entry.path === join44(targetMapping.dest, request.name);
12001
13163
  });
12002
13164
  const status = manifestEntry ? "managed" : exists ? "present-unmanaged" : "missing";
12003
13165
  skills.push({
@@ -12077,7 +13239,7 @@ function doctorSkillLabel(name) {
12077
13239
  return `${name} skill`;
12078
13240
  }
12079
13241
  function isSyncwheelWorkspace(targetRoot) {
12080
- return existsSync(join41(targetRoot, ".syncwheel", "manifest.json"));
13242
+ return existsSync(join44(targetRoot, ".syncwheel", "manifest.json"));
12081
13243
  }
12082
13244
  function skillInstallCommand(adapter, installationType, options, skill, behavior = {}) {
12083
13245
  const args = [
@@ -12166,11 +13328,11 @@ function looksLikeSourceSpecifier(value) {
12166
13328
  }
12167
13329
  function normalizeCliPath(value) {
12168
13330
  if (value === "~") return homedir9();
12169
- if (value.startsWith("~/")) return resolve20(homedir9(), value.slice(2));
12170
- return resolve20(value);
13331
+ if (value.startsWith("~/")) return resolve22(homedir9(), value.slice(2));
13332
+ return resolve22(value);
12171
13333
  }
12172
13334
  function isHomePath(path) {
12173
- return resolve20(path) === resolve20(homedir9());
13335
+ return resolve22(path) === resolve22(homedir9());
12174
13336
  }
12175
13337
  function adapterListFromOption(adapter) {
12176
13338
  if (!adapter) return [];
@@ -12225,10 +13387,10 @@ function filterUninstallPlanBySelection(plan, selected) {
12225
13387
  };
12226
13388
  }
12227
13389
  async function initPackage(root) {
12228
- await mkdir22(join41(root, "instructions"), { recursive: true });
12229
- await mkdir22(join41(root, "rules"), { recursive: true });
12230
- await mkdir22(join41(root, "skills"), { recursive: true });
12231
- const manifestPath = join41(root, "openpack.json");
13390
+ await mkdir23(join44(root, "instructions"), { recursive: true });
13391
+ await mkdir23(join44(root, "rules"), { recursive: true });
13392
+ await mkdir23(join44(root, "skills"), { recursive: true });
13393
+ const manifestPath = join44(root, "openpack.json");
12232
13394
  const manifest = {
12233
13395
  schemaVersion: 2,
12234
13396
  name: "example/agentwheel-package",
@@ -12239,12 +13401,12 @@ async function initPackage(root) {
12239
13401
  { type: "skills", path: "skills" }
12240
13402
  ]
12241
13403
  };
12242
- await writeFile21(manifestPath, `${JSON.stringify(manifest, null, 2)}
13404
+ await writeFile22(manifestPath, `${JSON.stringify(manifest, null, 2)}
12243
13405
  `, "utf8");
12244
- await writeFile21(join41(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
13406
+ await writeFile22(join44(root, "instructions", "AGENTS.md"), "# Agent Instructions\n", "utf8");
12245
13407
  }
12246
13408
  async function defaultBootstrapPackage(_root) {
12247
- const packageRoot = await findAgentwheelPackageRoot(dirname31(fileURLToPath3(import.meta.url)));
13409
+ const packageRoot = await findAgentwheelPackageRoot(dirname32(fileURLToPath3(import.meta.url)));
12248
13410
  if (!packageRoot) return void 0;
12249
13411
  return {
12250
13412
  name: "agentwheel",
@@ -12288,10 +13450,10 @@ function withFleetExample(config) {
12288
13450
  };
12289
13451
  }
12290
13452
  async function findAgentwheelPackageRoot(start) {
12291
- let current = resolve20(start);
13453
+ let current = resolve22(start);
12292
13454
  while (true) {
12293
13455
  if (await findPackageManifestPath(current, { warnLegacy: false })) return current;
12294
- const parent = dirname31(current);
13456
+ const parent = dirname32(current);
12295
13457
  if (parent === current) return void 0;
12296
13458
  current = parent;
12297
13459
  }