@rness/cli 0.6.1 → 0.7.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.
@@ -735,6 +735,9 @@ async function isClean(dir, ignore = []) {
735
735
  [
736
736
  "status",
737
737
  "--porcelain",
738
+ // Every untracked file on its own line: a directory folded into one
739
+ // `?? dir/` entry would slip past a pathspec naming a file inside it.
740
+ "--untracked-files=all",
738
741
  ...pathspec.length > 0 ? ["--", ...pathspec] : []
739
742
  ],
740
743
  dir
@@ -857,7 +860,7 @@ function isWorkspaceClone(name) {
857
860
  }
858
861
  var NAME_RULE = "lowercase letters, digits, '.', '_' and '-', not starting with '-'";
859
862
  var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
860
- var KEYS = ["contract", "org", "repos", "scopes"];
863
+ var KEYS = ["contract", "org", "agents", "repos", "scopes"];
861
864
  function fail(message) {
862
865
  throw new Error(`rness.json: ${message}`);
863
866
  }
@@ -886,6 +889,15 @@ function readOrg(value) {
886
889
  function workspaceName(manifest, root) {
887
890
  return manifest.org ?? basename2(root);
888
891
  }
892
+ var AGENT_NAME = /^[a-z][a-z0-9-]*$/;
893
+ function readAgents(value) {
894
+ if (value === void 0) return null;
895
+ if (!Array.isArray(value) || !value.every((v) => typeof v === "string" && AGENT_NAME.test(v)) || new Set(value).size !== value.length)
896
+ fail(
897
+ `"agents" must be a list of unique agent names (got ${JSON.stringify(value)})`
898
+ );
899
+ return [...value];
900
+ }
889
901
  function readRepos(value) {
890
902
  const raw = value ?? {};
891
903
  if (!isRecord(raw)) fail('"repos" must be an object');
@@ -944,6 +956,7 @@ async function loadManifest(rnessDir) {
944
956
  return {
945
957
  contract: 1,
946
958
  org: readOrg(data.org),
959
+ agents: readAgents(data.agents),
947
960
  repos: readRepos(data.repos),
948
961
  scopes: readScopes(data.scopes)
949
962
  };
@@ -959,6 +972,10 @@ async function writeManifest(rnessDir, manifest) {
959
972
  const lines = ["{", ' "contract": 1,'];
960
973
  if (manifest.org !== null)
961
974
  lines.push(` "org": ${JSON.stringify(manifest.org)},`);
975
+ if (manifest.agents !== null)
976
+ lines.push(
977
+ ` "agents": [${manifest.agents.map((a) => JSON.stringify(a)).join(", ")}],`
978
+ );
962
979
  lines.push(
963
980
  ' "repos": {',
964
981
  repos.join(",\n"),
@@ -1045,13 +1062,13 @@ async function addRepository(input) {
1045
1062
  await clone(url, dir, input.credentialsFor?.(url) ?? null);
1046
1063
  action = "cloned";
1047
1064
  }
1048
- const declared = [name];
1065
+ const declared2 = [name];
1049
1066
  for (const { path, base } of subs) {
1050
1067
  if (!await exists(join4(input.root, ...path.split("/")))) {
1051
1068
  if (action === "cloned") await rm3(dir, { recursive: true, force: true });
1052
1069
  throw new Error(`${path} does not exist`);
1053
1070
  }
1054
- declared.push(base);
1071
+ declared2.push(base);
1055
1072
  }
1056
1073
  const manifest = {
1057
1074
  ...input.manifest,
@@ -1059,7 +1076,7 @@ async function addRepository(input) {
1059
1076
  scopes
1060
1077
  };
1061
1078
  await writeManifest(input.rnessDir, manifest);
1062
- return { manifest, name, url, action, declared };
1079
+ return { manifest, name, url, action, declared: declared2 };
1063
1080
  }
1064
1081
  async function workspaceDirs(repoDir) {
1065
1082
  let patterns = [];
@@ -1372,6 +1389,9 @@ function reportError(e) {
1372
1389
  return 1;
1373
1390
  }
1374
1391
 
1392
+ // src/version.ts
1393
+ var VERSION = "0.7.0";
1394
+
1375
1395
  // src/core/block.ts
1376
1396
  import { createHash } from "crypto";
1377
1397
  var BEGIN = "<!-- BEGIN rness -->";
@@ -1637,6 +1657,17 @@ function mergeBlock(existing, block) {
1637
1657
  }
1638
1658
  return { ok: true, text, changed: text !== existing };
1639
1659
  }
1660
+ var POINTER = "@AGENTS.md";
1661
+ function claudeMdAtRoot(existing, block) {
1662
+ if (existing === null) return mergeBlock(null, block);
1663
+ const lines = existing.split(/\r?\n/);
1664
+ if (!lines.some((l) => l.trim() === POINTER))
1665
+ return mergeBlock(existing, block);
1666
+ const eol = existing.includes("\r\n") ? "\r\n" : "\n";
1667
+ const rest = lines.filter((l) => l.trim() !== POINTER).join(eol);
1668
+ const merged = mergeBlock(rest.trim() === "" ? null : rest, block);
1669
+ return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
1670
+ }
1640
1671
  async function ensureClaudeMd(dir) {
1641
1672
  const file = join8(dir, "CLAUDE.md");
1642
1673
  if (await isSymlink(file)) return "unchanged";
@@ -1653,7 +1684,214 @@ async function ensureClaudeMd(dir) {
1653
1684
  }
1654
1685
 
1655
1686
  // src/commands/sync.ts
1656
- import { join as join9 } from "path";
1687
+ import { join as join10 } from "path";
1688
+
1689
+ // src/core/agent-targets.ts
1690
+ import { mkdir as mkdir2 } from "fs/promises";
1691
+ import { dirname as dirname4, join as join9 } from "path";
1692
+
1693
+ // src/core/agents.ts
1694
+ var TARGETS = {
1695
+ claude: {
1696
+ name: "claude",
1697
+ label: "Claude Code",
1698
+ file: ".claude/settings.json",
1699
+ // Read the context repository from a repository without a prompt. The
1700
+ // path is resolved against the project directory, and only once Claude
1701
+ // Code has trusted it (verified on 2.1.284, spec 0011 §7).
1702
+ guarantees: [
1703
+ {
1704
+ path: ["permissions", "additionalDirectories"],
1705
+ contains: "../../.rness"
1706
+ }
1707
+ ]
1708
+ }
1709
+ };
1710
+ var SUPPORTED_AGENTS = Object.keys(TARGETS);
1711
+ function unsupportedAgents(names) {
1712
+ return names.filter((n) => !Object.hasOwn(TARGETS, n));
1713
+ }
1714
+ function unsupportedMessage(name) {
1715
+ return `rness.json: agent "${name}" is not supported by @rness/cli ${VERSION} (supported: ${SUPPORTED_AGENTS.join(", ")})`;
1716
+ }
1717
+
1718
+ // src/core/json-guarantees.ts
1719
+ function isObject(value) {
1720
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1721
+ }
1722
+ function parse3(text) {
1723
+ let data;
1724
+ try {
1725
+ data = JSON.parse(text);
1726
+ } catch {
1727
+ return { ok: false, reason: "not valid JSON" };
1728
+ }
1729
+ return isObject(data) ? { ok: true, data } : { ok: false, reason: "not a JSON object" };
1730
+ }
1731
+ function listAt(root, g, create) {
1732
+ let node = root;
1733
+ for (const [i, key] of g.path.slice(0, -1).entries()) {
1734
+ const next = node[key];
1735
+ if (next === void 0) {
1736
+ if (!create) return { kind: "absent" };
1737
+ node[key] = {};
1738
+ } else if (!isObject(next))
1739
+ return {
1740
+ kind: "invalid",
1741
+ reason: `${g.path.slice(0, i + 1).join(".")} is not an object`
1742
+ };
1743
+ node = node[key];
1744
+ }
1745
+ const last = g.path[g.path.length - 1] ?? "";
1746
+ const value = node[last];
1747
+ if (value === void 0) {
1748
+ if (!create) return { kind: "absent" };
1749
+ const list = [];
1750
+ node[last] = list;
1751
+ return { kind: "list", list };
1752
+ }
1753
+ if (!Array.isArray(value))
1754
+ return { kind: "invalid", reason: `${g.path.join(".")} is not a list` };
1755
+ return { kind: "list", list: value };
1756
+ }
1757
+ function styleOf(text) {
1758
+ const indent = text.split(/\r?\n/).slice(1).map((l) => /^[ \t]+/.exec(l)?.[0]).find((w) => w !== void 0);
1759
+ return {
1760
+ indent: indent ?? " ",
1761
+ eol: text.includes("\r\n") ? "\r\n" : "\n",
1762
+ final: text.endsWith("\n")
1763
+ };
1764
+ }
1765
+ function ensureGuarantees(existing, guarantees) {
1766
+ const parsed = existing === null ? { ok: true, data: {} } : parse3(existing);
1767
+ if (!parsed.ok) return { kind: "invalid", reason: parsed.reason };
1768
+ let changed = false;
1769
+ for (const g of guarantees) {
1770
+ const found = listAt(parsed.data, g, true);
1771
+ if (found.kind === "invalid")
1772
+ return { kind: "invalid", reason: found.reason };
1773
+ if (found.kind === "list" && !found.list.includes(g.contains)) {
1774
+ found.list.push(g.contains);
1775
+ changed = true;
1776
+ }
1777
+ }
1778
+ if (existing === null)
1779
+ return {
1780
+ kind: "created",
1781
+ text: `${JSON.stringify(parsed.data, null, 2)}
1782
+ `
1783
+ };
1784
+ if (!changed) return { kind: "unchanged", text: existing };
1785
+ const { indent, eol, final } = styleOf(existing);
1786
+ const body = JSON.stringify(parsed.data, null, indent).split("\n").join(eol);
1787
+ return { kind: "updated", text: final ? `${body}${eol}` : body };
1788
+ }
1789
+ function missingGuarantees(existing, guarantees) {
1790
+ if (existing === null) return [...guarantees];
1791
+ const parsed = parse3(existing);
1792
+ if (!parsed.ok) return { invalid: parsed.reason };
1793
+ const missing = [];
1794
+ for (const g of guarantees) {
1795
+ const found = listAt(parsed.data, g, false);
1796
+ if (found.kind === "invalid") return { invalid: found.reason };
1797
+ if (found.kind === "absent" || !found.list.includes(g.contains))
1798
+ missing.push(g);
1799
+ }
1800
+ return missing;
1801
+ }
1802
+ function describeGuarantee(g) {
1803
+ return `${g.path.join(".")} lacks ${g.contains}`;
1804
+ }
1805
+
1806
+ // src/core/agent-targets.ts
1807
+ async function filesOf(root, manifest, targets) {
1808
+ const files = [];
1809
+ for (const repo of Object.keys(manifest.repos)) {
1810
+ if (!await exists(join9(root, "org", repo))) continue;
1811
+ for (const target of targets)
1812
+ files.push({
1813
+ target,
1814
+ file: join9(root, "org", repo, ...target.file.split("/")),
1815
+ label: `org/${repo}/${target.file}`
1816
+ });
1817
+ }
1818
+ return files;
1819
+ }
1820
+ function declared(manifest) {
1821
+ return (manifest.agents ?? []).flatMap(
1822
+ (name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
1823
+ );
1824
+ }
1825
+ async function agentTargets(root, manifest, opts) {
1826
+ const outcomes = [];
1827
+ for (const { target, file, label } of await filesOf(
1828
+ root,
1829
+ manifest,
1830
+ declared(manifest)
1831
+ )) {
1832
+ const existing = await readOrNull(file);
1833
+ if (opts.check) {
1834
+ const missing = missingGuarantees(existing, target.guarantees);
1835
+ if ("invalid" in missing)
1836
+ outcomes.push({ label, status: "invalid", detail: missing.invalid });
1837
+ else if (missing.length > 0)
1838
+ outcomes.push({
1839
+ label,
1840
+ status: "stale",
1841
+ detail: missing.map(describeGuarantee).join("; ")
1842
+ });
1843
+ else outcomes.push({ label, status: "unchanged", detail: null });
1844
+ continue;
1845
+ }
1846
+ const ensured = ensureGuarantees(existing, target.guarantees);
1847
+ if (ensured.kind === "invalid") {
1848
+ outcomes.push({ label, status: "invalid", detail: ensured.reason });
1849
+ continue;
1850
+ }
1851
+ if (ensured.kind === "unchanged") {
1852
+ outcomes.push({ label, status: "unchanged", detail: null });
1853
+ continue;
1854
+ }
1855
+ await mkdir2(dirname4(file), { recursive: true });
1856
+ await writeFileAtomic(file, ensured.text);
1857
+ outcomes.push({ label, status: "updated", detail: null });
1858
+ }
1859
+ return outcomes;
1860
+ }
1861
+ async function leftoverTargets(root, manifest) {
1862
+ if (manifest.agents === null) return [];
1863
+ const agents = manifest.agents;
1864
+ const undeclared = Object.values(TARGETS).filter(
1865
+ (t) => !agents.includes(t.name)
1866
+ );
1867
+ const leftovers = [];
1868
+ for (const { target, file, label } of await filesOf(
1869
+ root,
1870
+ manifest,
1871
+ undeclared
1872
+ )) {
1873
+ const existing = await readOrNull(file);
1874
+ if (existing === null) continue;
1875
+ const missing = missingGuarantees(existing, target.guarantees);
1876
+ if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
1877
+ }
1878
+ return leftovers;
1879
+ }
1880
+
1881
+ // src/core/ask-agents.ts
1882
+ var AGENTS_QUESTION = "Which agents does your team use?";
1883
+ async function askAgents(p) {
1884
+ const answer = await p.multiselect({
1885
+ message: AGENTS_QUESTION,
1886
+ options: Object.values(TARGETS).map((t) => ({
1887
+ value: t.name,
1888
+ label: t.label
1889
+ })),
1890
+ required: false
1891
+ });
1892
+ if (p.isCancel(answer)) return null;
1893
+ return Array.isArray(answer) ? answer : [];
1894
+ }
1657
1895
 
1658
1896
  // src/core/progress.ts
1659
1897
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -1842,13 +2080,25 @@ function targetsOf(manifest, root, only) {
1842
2080
  const segments = entry.path.split("/");
1843
2081
  targets.push({
1844
2082
  scope: name,
1845
- dir: join9(root, ...segments),
2083
+ dir: join10(root, ...segments),
1846
2084
  depth: segments.length,
1847
2085
  label: `${entry.path}/AGENTS.md`
1848
2086
  });
1849
2087
  }
1850
2088
  return targets;
1851
2089
  }
2090
+ async function syncBlockFile(file, block, check, rootClaudeMd = false) {
2091
+ const existing = await readOrNull(file);
2092
+ const existingLines = existing === null ? [] : existing.split(/\r?\n/);
2093
+ const span = findBlock(existingLines);
2094
+ const current = span.kind === "one" && isCurrentBlock(existingLines.slice(span.begin, span.end + 1), block.hash) && !(rootClaudeMd && existingLines.some((l) => l.trim() === "@AGENTS.md"));
2095
+ const merged = current ? { ok: true, text: existing ?? "", changed: false } : rootClaudeMd ? claudeMdAtRoot(existing, block.text) : mergeBlock(existing, block.text);
2096
+ if (!merged.ok) return { error: merged.error };
2097
+ if (!merged.changed) return "unchanged";
2098
+ if (check) return "stale";
2099
+ await writeFileAtomic(file, merged.text);
2100
+ return "updated";
2101
+ }
1852
2102
  var SELECT_ALL = "*";
1853
2103
  async function askWhatToSync(input) {
1854
2104
  if (!input.terminal.isTty()) {
@@ -1894,16 +2144,60 @@ async function syncCommand(opts, deps = {}) {
1894
2144
  const check = opts.check === true;
1895
2145
  const ownUi = deps.ui === void 0;
1896
2146
  const ui = deps.ui ?? (opts.yes !== true && !check ? await makeUi(terminal) : { ...plainUi });
2147
+ const agentFlags = opts.agent ?? [];
2148
+ if (agentFlags.length > 0 && check) {
2149
+ process.stderr.write(
2150
+ "--agent cannot be combined with --check: it declares agents in rness.json\n"
2151
+ );
2152
+ return 2;
2153
+ }
2154
+ const unknown = unsupportedAgents(agentFlags)[0];
2155
+ if (unknown !== void 0) {
2156
+ process.stderr.write(
2157
+ `unknown agent "${unknown}" (supported: ${SUPPORTED_AGENTS.join(", ")})
2158
+ `
2159
+ );
2160
+ return 2;
2161
+ }
1897
2162
  try {
1898
2163
  const ws = await findWorkspace(cwd);
1899
2164
  const manifest = await loadManifest(ws.rnessDir);
1900
2165
  const org = workspaceName(manifest, ws.root);
2166
+ const unsupported = unsupportedAgents(manifest.agents ?? [])[0];
2167
+ if (unsupported !== void 0) {
2168
+ process.stderr.write(`${unsupportedMessage(unsupported)}
2169
+ `);
2170
+ return 1;
2171
+ }
1901
2172
  if (ownUi) ui.intro(`Sync workspace ${org}`);
1902
2173
  if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
1903
2174
  process.stderr.write(`unknown scope: ${opts.scope}
1904
2175
  `);
1905
2176
  return 1;
1906
2177
  }
2178
+ let declare = null;
2179
+ if (agentFlags.length > 0) {
2180
+ const current = manifest.agents ?? [];
2181
+ const added = agentFlags.filter((a) => !current.includes(a));
2182
+ if (added.length > 0) declare = [...current, ...new Set(added)];
2183
+ } else if (manifest.agents === null && !check && opts.yes !== true && opts.scope === void 0 && terminal.isTty()) {
2184
+ const answer = await askAgents(await terminal.prompts());
2185
+ if (answer === null) {
2186
+ ui.cancelled();
2187
+ return 1;
2188
+ }
2189
+ declare = answer;
2190
+ }
2191
+ if (declare !== null) {
2192
+ const before = manifest.agents ?? [];
2193
+ manifest.agents = declare;
2194
+ await writeManifest(ws.rnessDir, manifest);
2195
+ for (const name of declare.filter((a) => !before.includes(a)))
2196
+ ui.line("declared", `agent ${name} in .rness/rness.json`);
2197
+ if (declare.length === 0)
2198
+ ui.line("declared", "no agent in .rness/rness.json");
2199
+ ui.hint("rness.json changed: commit it in .rness, for the whole team");
2200
+ }
1907
2201
  const clones = await clonesIn(ws.root);
1908
2202
  const missing = Object.keys(manifest.repos).filter((n) => !clones.has(n));
1909
2203
  let toClone = new Set(opts.all === true && !check ? missing : []);
@@ -1945,7 +2239,7 @@ async function syncCommand(opts, deps = {}) {
1945
2239
  try {
1946
2240
  const notCloned = /* @__PURE__ */ new Set();
1947
2241
  for (const [name, repo] of Object.entries(manifest.repos)) {
1948
- const dir = join9(ws.root, "org", name);
2242
+ const dir = join10(ws.root, "org", name);
1949
2243
  const label = `org/${name}`;
1950
2244
  if (!await exists(dir)) {
1951
2245
  if (!toClone.has(name)) {
@@ -1966,7 +2260,13 @@ async function syncCommand(opts, deps = {}) {
1966
2260
  }
1967
2261
  } else if (opts.pull === true && !check) {
1968
2262
  try {
1969
- if (!await isClean(dir, MANAGED_FILES)) {
2263
+ const written = [
2264
+ ...MANAGED_FILES,
2265
+ ...(manifest.agents ?? []).flatMap(
2266
+ (a) => Object.hasOwn(TARGETS, a) ? [TARGETS[a].file] : []
2267
+ )
2268
+ ];
2269
+ if (!await isClean(dir, written)) {
1970
2270
  lines.push(["skipped", `${label} (working tree not clean)`]);
1971
2271
  continue;
1972
2272
  }
@@ -1997,7 +2297,7 @@ async function syncCommand(opts, deps = {}) {
1997
2297
  lines.push(
1998
2298
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
1999
2299
  );
2000
- if (!await exists(join9(ws.root, "org"))) {
2300
+ if (!await exists(join10(ws.root, "org"))) {
2001
2301
  lines.push(["skipped", "blocks (no org/ directory here)"]);
2002
2302
  } else {
2003
2303
  let differences = 0;
@@ -2008,10 +2308,11 @@ async function syncCommand(opts, deps = {}) {
2008
2308
  lines.push(["skipped", `${t.label} (directory not present)`]);
2009
2309
  continue;
2010
2310
  }
2011
- const linked = await Promise.all(
2012
- MANAGED_FILES.map((f) => isSymlink(join9(t.dir, f)))
2311
+ const [agentsLinked, claudeLinked] = await Promise.all(
2312
+ MANAGED_FILES.map((f) => isSymlink(join10(t.dir, f)))
2013
2313
  );
2014
- if (linked.includes(true)) {
2314
+ const atRoot = t.scope === null;
2315
+ if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
2015
2316
  lines.push(["skipped", `${t.label} (symlink)`]);
2016
2317
  continue;
2017
2318
  }
@@ -2031,37 +2332,59 @@ async function syncCommand(opts, deps = {}) {
2031
2332
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
2032
2333
  );
2033
2334
  }
2034
- const file = join9(t.dir, "AGENTS.md");
2035
- const existing = await readOrNull(file);
2036
- const existingLines = existing === null ? [] : existing.split(/\r?\n/);
2037
- const span = findBlock(existingLines);
2038
- const current = span.kind === "one" && isCurrentBlock(
2039
- existingLines.slice(span.begin, span.end + 1),
2040
- block.hash
2041
- );
2042
- const merged = current ? { ok: true, text: existing ?? "", changed: false } : mergeBlock(existing, block.text);
2043
- if (!merged.ok) {
2044
- problems.push(`${t.label}: ${merged.error}`);
2335
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join10(t.dir, "AGENTS.md"), block, check);
2336
+ if (typeof agents === "object") {
2337
+ problems.push(`${t.label}: ${agents.error}`);
2045
2338
  continue;
2046
2339
  }
2047
- if (!merged.changed) {
2048
- lines.push(["unchanged", `${t.label}`]);
2340
+ lines.push(
2341
+ agents === "skipped" ? ["skipped", `${t.label} (symlink)`] : [agents, t.label]
2342
+ );
2343
+ if (agents === "stale") differences += 1;
2344
+ if (!atRoot) {
2049
2345
  if (!check) await ensureClaudeMd(t.dir);
2050
2346
  continue;
2051
2347
  }
2052
- if (check) {
2053
- lines.push(["stale", `${t.label}`]);
2054
- differences += 1;
2348
+ if (claudeLinked === true) continue;
2349
+ const claude = await syncBlockFile(
2350
+ join10(t.dir, "CLAUDE.md"),
2351
+ block,
2352
+ check,
2353
+ true
2354
+ );
2355
+ if (typeof claude === "object") {
2356
+ problems.push(`CLAUDE.md: ${claude.error}`);
2055
2357
  continue;
2056
2358
  }
2057
- await writeFileAtomic(file, merged.text);
2058
- await ensureClaudeMd(t.dir);
2059
- lines.push(["updated", `${t.label}`]);
2359
+ if (claude === "stale") differences += 1;
2360
+ if (claude !== agents) lines.push([claude, "CLAUDE.md"]);
2060
2361
  }
2061
2362
  if (check && differences > 0)
2062
2363
  problems.push(
2063
2364
  `${differences} block(s) out of date \u2014 run ${rnessCommand()} sync`
2064
2365
  );
2366
+ if (opts.scope === void 0) {
2367
+ let stale = 0;
2368
+ for (const o of await agentTargets(ws.root, manifest, { check })) {
2369
+ if (o.status === "invalid")
2370
+ problems.push(
2371
+ `${o.label}: ${o.detail ?? "unreadable"}; fix it, then ${rnessCommand()} sync`
2372
+ );
2373
+ else if (o.status === "stale") {
2374
+ stale += 1;
2375
+ lines.push(["stale", `${o.label} (${o.detail ?? ""})`]);
2376
+ } else lines.push([o.status, o.label]);
2377
+ }
2378
+ if (stale > 0)
2379
+ problems.push(
2380
+ `${stale} agent file(s) out of date \u2014 run ${rnessCommand()} sync`
2381
+ );
2382
+ if (!check)
2383
+ for (const label of await leftoverTargets(ws.root, manifest))
2384
+ lines.push(
2385
+ `${label} still carries the values of an agent no longer in rness.json (left as is)`
2386
+ );
2387
+ }
2065
2388
  }
2066
2389
  return problems.length > 0 ? 1 : 0;
2067
2390
  } finally {
@@ -2081,7 +2404,7 @@ async function syncCommand(opts, deps = {}) {
2081
2404
  }
2082
2405
 
2083
2406
  // src/commands/add.ts
2084
- import { basename as basename3, join as join10 } from "path";
2407
+ import { basename as basename3, join as join11 } from "path";
2085
2408
  function printDeclared(ui, result, already) {
2086
2409
  for (const name of result.declared) {
2087
2410
  if (already.has(name)) continue;
@@ -2174,7 +2497,7 @@ async function addCommand(spec, opts, deps = {}) {
2174
2497
  }
2175
2498
  parsed = parseRepoSpec(spec, org, host);
2176
2499
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
2177
- const dir = join10(ws.root, "org", parsed.name);
2500
+ const dir = join11(ws.root, "org", parsed.name);
2178
2501
  const present = await exists(dir);
2179
2502
  if (interactive) {
2180
2503
  const { confirm, isCancel } = await terminal.prompts();
@@ -2300,26 +2623,23 @@ async function contextCommand(opts) {
2300
2623
  }
2301
2624
  }
2302
2625
 
2303
- // src/version.ts
2304
- var VERSION = "0.6.1";
2305
-
2306
2626
  // src/core/delegate.ts
2307
2627
  import { readFile as readFile6 } from "fs/promises";
2308
- import { join as join11 } from "path";
2628
+ import { join as join12 } from "path";
2309
2629
  var DEFAULT_BIN = "dist/bin/rness.js";
2310
2630
  async function readPinnedCli(rnessDir) {
2311
- const dir = join11(rnessDir, "node_modules", "@rness", "cli");
2631
+ const dir = join12(rnessDir, "node_modules", "@rness", "cli");
2312
2632
  let pkg;
2313
2633
  try {
2314
- pkg = JSON.parse(await readFile6(join11(dir, "package.json"), "utf8"));
2634
+ pkg = JSON.parse(await readFile6(join12(dir, "package.json"), "utf8"));
2315
2635
  } catch {
2316
2636
  return null;
2317
2637
  }
2318
- const declared = pkg.bin;
2638
+ const declared2 = pkg.bin;
2319
2639
  let bin = DEFAULT_BIN;
2320
- if (typeof declared === "string") bin = declared;
2321
- else if (declared !== null && typeof declared === "object") {
2322
- const named = declared["rness"];
2640
+ if (typeof declared2 === "string") bin = declared2;
2641
+ else if (declared2 !== null && typeof declared2 === "object") {
2642
+ const named = declared2["rness"];
2323
2643
  if (typeof named === "string") bin = named;
2324
2644
  }
2325
2645
  return {
@@ -2331,7 +2651,7 @@ async function readPinnedCli(rnessDir) {
2331
2651
  async function pinnedBinPath(rnessDir) {
2332
2652
  const pinned = await readPinnedCli(rnessDir);
2333
2653
  if (pinned === null) return null;
2334
- return join11(pinned.dir, ...pinned.bin.split("/"));
2654
+ return join12(pinned.dir, ...pinned.bin.split("/"));
2335
2655
  }
2336
2656
  async function findDelegate(cwd, ownVersion) {
2337
2657
  let root;
@@ -2347,12 +2667,12 @@ async function findDelegate(cwd, ownVersion) {
2347
2667
  if (pinned === null) return null;
2348
2668
  const { version } = pinned;
2349
2669
  if (version === null || version === ownVersion) return null;
2350
- return { entry: join11(pinned.dir, "dist", "index.js"), version, root };
2670
+ return { entry: join12(pinned.dir, "dist", "index.js"), version, root };
2351
2671
  }
2352
2672
 
2353
2673
  // src/core/scaffold.ts
2354
2674
  import { statSync } from "fs";
2355
- import { dirname as dirname4, join as join12 } from "path";
2675
+ import { dirname as dirname5, join as join13 } from "path";
2356
2676
  import { fileURLToPath } from "url";
2357
2677
  var SCAFFOLD_FILES = [
2358
2678
  "AGENTS.md",
@@ -2374,30 +2694,30 @@ var SCAFFOLD_FILES = [
2374
2694
  "docs/.gitkeep"
2375
2695
  ];
2376
2696
  function scaffoldDir() {
2377
- let dir = dirname4(fileURLToPath(import.meta.url));
2697
+ let dir = dirname5(fileURLToPath(import.meta.url));
2378
2698
  for (let i = 0; i < 4; i += 1) {
2379
- const candidate = join12(dir, "scaffold");
2699
+ const candidate = join13(dir, "scaffold");
2380
2700
  try {
2381
- if (statSync(join12(candidate, "rness.json")).isFile()) return candidate;
2701
+ if (statSync(join13(candidate, "rness.json")).isFile()) return candidate;
2382
2702
  } catch {
2383
2703
  }
2384
- dir = dirname4(dir);
2704
+ dir = dirname5(dir);
2385
2705
  }
2386
2706
  throw new Error("scaffold directory not found next to the @rness/cli package");
2387
2707
  }
2388
2708
 
2389
2709
  // src/core/scaffold-copy.ts
2390
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2391
- import { dirname as dirname5, join as join13 } from "path";
2710
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2711
+ import { dirname as dirname6, join as join14 } from "path";
2392
2712
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
2393
2713
  async function copyScaffold(dest, tokens) {
2394
2714
  const source = scaffoldDir();
2395
2715
  for (const rel of SCAFFOLD_FILES) {
2396
2716
  if (rel === "rness.json") continue;
2397
- const from = join13(source, ...rel.split("/"));
2717
+ const from = join14(source, ...rel.split("/"));
2398
2718
  const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
2399
- const to = join13(dest, ...targetRel.split("/"));
2400
- await mkdir2(dirname5(to), { recursive: true });
2719
+ const to = join14(dest, ...targetRel.split("/"));
2720
+ await mkdir3(dirname6(to), { recursive: true });
2401
2721
  let content = await readFile7(from, "utf8");
2402
2722
  if (TOKENISED.has(rel)) {
2403
2723
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
@@ -2558,18 +2878,18 @@ function revokeUrl() {
2558
2878
  }
2559
2879
 
2560
2880
  // src/commands/create.ts
2561
- import { mkdir as mkdir5, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
2881
+ import { mkdir as mkdir6, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
2562
2882
  import { tmpdir } from "os";
2563
- import { join as join17, relative as relative4, resolve as resolve4 } from "path";
2883
+ import { join as join18, relative as relative4, resolve as resolve4 } from "path";
2564
2884
 
2565
2885
  // src/core/new-workspace.ts
2566
- import { mkdir as mkdir3, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
2567
- import { dirname as dirname6, join as join15, relative as relative2 } from "path";
2886
+ import { mkdir as mkdir4, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
2887
+ import { dirname as dirname7, join as join16, relative as relative2 } from "path";
2568
2888
 
2569
2889
  // src/core/pinned.ts
2570
2890
  import { execFile as execFile4 } from "child_process";
2571
2891
  import { readFile as readFile8 } from "fs/promises";
2572
- import { basename as basename4, join as join14 } from "path";
2892
+ import { basename as basename4, join as join15 } from "path";
2573
2893
  import { promisify as promisify4 } from "util";
2574
2894
  var execFileP4 = promisify4(execFile4);
2575
2895
  var PACKAGE = "@rness/cli";
@@ -2594,7 +2914,7 @@ function compareVersions(a, b) {
2594
2914
  async function readPackageJson(rnessDir) {
2595
2915
  try {
2596
2916
  const parsed = JSON.parse(
2597
- await readFile8(join14(rnessDir, "package.json"), "utf8")
2917
+ await readFile8(join15(rnessDir, "package.json"), "utf8")
2598
2918
  );
2599
2919
  return parsed !== null && typeof parsed === "object" ? parsed : null;
2600
2920
  } catch {
@@ -2613,7 +2933,7 @@ async function readPin(rnessDir) {
2613
2933
  return null;
2614
2934
  }
2615
2935
  async function writePin(rnessDir, from, to) {
2616
- const file = join14(rnessDir, "package.json");
2936
+ const file = join15(rnessDir, "package.json");
2617
2937
  const text = await readFile8(file, "utf8");
2618
2938
  const entry = new RegExp(
2619
2939
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
@@ -2630,18 +2950,18 @@ var LOCKFILES = [
2630
2950
  ["package-lock.json", "npm"]
2631
2951
  ];
2632
2952
  async function workspacePackageManager(rnessDir) {
2633
- const declared = (await readPackageJson(rnessDir))?.["packageManager"];
2634
- if (typeof declared === "string") {
2635
- const name = declared.split("@")[0] ?? "";
2953
+ const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
2954
+ if (typeof declared2 === "string") {
2955
+ const name = declared2.split("@")[0] ?? "";
2636
2956
  if (isPackageManager(name)) return name;
2637
2957
  }
2638
2958
  for (const [lockfile, pm] of LOCKFILES)
2639
- if (await exists(join14(rnessDir, lockfile))) return pm;
2959
+ if (await exists(join15(rnessDir, lockfile))) return pm;
2640
2960
  return "npm";
2641
2961
  }
2642
2962
  async function hasLockfile(rnessDir, pm) {
2643
2963
  for (const [lockfile, owner] of LOCKFILES)
2644
- if (owner === pm && await exists(join14(rnessDir, lockfile))) return true;
2964
+ if (owner === pm && await exists(join15(rnessDir, lockfile))) return true;
2645
2965
  return false;
2646
2966
  }
2647
2967
  async function pinDrift(rnessDir) {
@@ -2697,9 +3017,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
2697
3017
  }
2698
3018
  }
2699
3019
  async function runPinnedSync(root) {
2700
- const installed = await readPinnedCli(join14(root, ".rness"));
3020
+ const installed = await readPinnedCli(join15(root, ".rness"));
2701
3021
  if (installed === null) return null;
2702
- const pinned = join14(installed.dir, ...installed.bin.split("/"));
3022
+ const pinned = join15(installed.dir, ...installed.bin.split("/"));
2703
3023
  if (!await exists(pinned))
2704
3024
  return {
2705
3025
  code: 1,
@@ -2761,7 +3081,7 @@ function syncOutcome(result, opts = {}) {
2761
3081
  };
2762
3082
  }
2763
3083
  async function syncPinned(root, shown, fallback) {
2764
- const rnessDir = join14(root, ".rness");
3084
+ const rnessDir = join15(root, ".rness");
2765
3085
  const installed = await readPinnedCli(rnessDir);
2766
3086
  if (installed === null) {
2767
3087
  if (fallback !== void 0) return fallback();
@@ -2771,7 +3091,7 @@ async function syncPinned(root, shown, fallback) {
2771
3091
  );
2772
3092
  return 1;
2773
3093
  }
2774
- const pinned = join14(installed.dir, ...installed.bin.split("/"));
3094
+ const pinned = join15(installed.dir, ...installed.bin.split("/"));
2775
3095
  if (!await exists(pinned)) {
2776
3096
  process.stderr.write(
2777
3097
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -2818,11 +3138,11 @@ async function insideWorkspace(dir) {
2818
3138
  }
2819
3139
  async function targetProblem(cwd, root, pm) {
2820
3140
  const shown = relative2(cwd, root) || ".";
2821
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname6(root)))
3141
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname7(root)))
2822
3142
  return "already inside an rness workspace; create runs from outside one";
2823
3143
  if (!await exists(root)) return null;
2824
- if (await exists(join15(root, ".rness", "rness.json"))) {
2825
- if (!await exists(join15(root, ".rness", "node_modules")))
3144
+ if (await exists(join16(root, ".rness", "rness.json"))) {
3145
+ if (!await exists(join16(root, ".rness", "node_modules")))
2826
3146
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
2827
3147
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
2828
3148
  }
@@ -2845,9 +3165,9 @@ async function installContext(input) {
2845
3165
  }
2846
3166
  async function buildContext(input) {
2847
3167
  const { ui, root, shown, pm } = input;
2848
- const rnessDir = join15(root, ".rness");
3168
+ const rnessDir = join16(root, ".rness");
2849
3169
  const pmVersion = await packageManagerVersion(pm);
2850
- await mkdir3(root, { recursive: true });
3170
+ await mkdir4(root, { recursive: true });
2851
3171
  try {
2852
3172
  await copyScaffold(rnessDir, {
2853
3173
  version: VERSION,
@@ -2913,8 +3233,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
2913
3233
  }
2914
3234
 
2915
3235
  // src/commands/create-blank.ts
2916
- import { mkdir as mkdir4 } from "fs/promises";
2917
- import { join as join16, relative as relative3, resolve as resolve3 } from "path";
3236
+ import { mkdir as mkdir5 } from "fs/promises";
3237
+ import { join as join17, relative as relative3, resolve as resolve3 } from "path";
2918
3238
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
2919
3239
  function workspaceNameError(name) {
2920
3240
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -2960,6 +3280,14 @@ async function createBlank(input) {
2960
3280
  );
2961
3281
  return 2;
2962
3282
  }
3283
+ let agents = input.agents.length > 0 ? [...input.agents] : null;
3284
+ if (agents === null && input.interactive) {
3285
+ agents = await askAgents(await input.prompts());
3286
+ if (agents === null) {
3287
+ ui.cancelled();
3288
+ return 0;
3289
+ }
3290
+ }
2963
3291
  if (input.interactive && !prompted) {
2964
3292
  const p = await input.prompts();
2965
3293
  const ok = await p.confirm({
@@ -2974,14 +3302,14 @@ async function createBlank(input) {
2974
3302
  ui,
2975
3303
  root,
2976
3304
  shown,
2977
- manifest: { contract: 1, org: null, repos: {}, scopes: {} },
3305
+ manifest: { contract: 1, org: null, agents, repos: {}, scopes: {} },
2978
3306
  pm,
2979
3307
  skipInstall: input.skipInstall,
2980
3308
  kind: "blank workspace"
2981
3309
  });
2982
3310
  if (!built) return 1;
2983
- await mkdir4(join16(root, "org"), { recursive: true });
2984
- await commitContext({ ui, rnessDir: join16(root, ".rness"), shown });
3311
+ await mkdir5(join17(root, "org"), { recursive: true });
3312
+ await commitContext({ ui, rnessDir: join17(root, ".rness"), shown });
2985
3313
  const code = await syncBlocks(
2986
3314
  ui,
2987
3315
  root,
@@ -3141,7 +3469,7 @@ async function cloneSelection(input) {
3141
3469
  { doing: `cloning org/${repo}`, git: true },
3142
3470
  () => clone(
3143
3471
  entry.url,
3144
- join17(input.root, "org", repo),
3472
+ join18(input.root, "org", repo),
3145
3473
  input.provider.credentialsFor(entry.url)
3146
3474
  ),
3147
3475
  () => ["cloned", `org/${repo}`]
@@ -3272,6 +3600,15 @@ async function createCommand(opts, deps = {}) {
3272
3600
  `);
3273
3601
  return 2;
3274
3602
  }
3603
+ const agentFlags = [...new Set(opts.agent ?? [])];
3604
+ const unknownAgent = unsupportedAgents(agentFlags)[0];
3605
+ if (unknownAgent !== void 0) {
3606
+ process.stderr.write(
3607
+ `unknown agent "${unknownAgent}" (supported: ${SUPPORTED_AGENTS.join(", ")})
3608
+ `
3609
+ );
3610
+ return 2;
3611
+ }
3275
3612
  if (opts.ssh === true && opts.https === true) {
3276
3613
  process.stderr.write("--ssh and --https cannot be combined\n");
3277
3614
  return 2;
@@ -3345,6 +3682,7 @@ async function createCommand(opts, deps = {}) {
3345
3682
  if (blank)
3346
3683
  return await createBlank({
3347
3684
  name: opts.name,
3685
+ agents: agentFlags,
3348
3686
  cwd,
3349
3687
  pm,
3350
3688
  interactive,
@@ -3428,6 +3766,13 @@ async function createCommand(opts, deps = {}) {
3428
3766
  return 1;
3429
3767
  }
3430
3768
  const joining = probe.kind === "found";
3769
+ if (joining && agentFlags.length > 0) {
3770
+ process.stderr.write(
3771
+ `${org}/.rness exists: --agent only applies to a new workspace; after joining, run ${rnessCommand()} sync${agentFlags.map((a) => ` --agent ${a}`).join("")}
3772
+ `
3773
+ );
3774
+ return 2;
3775
+ }
3431
3776
  if (joining)
3432
3777
  ui.line(
3433
3778
  "found",
@@ -3440,10 +3785,16 @@ async function createCommand(opts, deps = {}) {
3440
3785
  `${org}/.rness (or not visible to you) \u2014 starting a new workspace`,
3441
3786
  `${org} has no workspace yet (or none you can see) \u2014 starting one`
3442
3787
  );
3443
- let catalogue = { contract: 1, org, repos: {}, scopes: {} };
3788
+ let catalogue = {
3789
+ contract: 1,
3790
+ org,
3791
+ agents: null,
3792
+ repos: {},
3793
+ scopes: {}
3794
+ };
3444
3795
  const staged = async () => {
3445
- staging ??= await mkdtemp(join17(tmpdir(), "rness-join-"));
3446
- return join17(staging, ".rness");
3796
+ staging ??= await mkdtemp(join18(tmpdir(), "rness-join-"));
3797
+ return join18(staging, ".rness");
3447
3798
  };
3448
3799
  if (joining) {
3449
3800
  const stagedDir = await staged();
@@ -3486,6 +3837,14 @@ async function createCommand(opts, deps = {}) {
3486
3837
  } else {
3487
3838
  specs = Object.keys(catalogue.repos);
3488
3839
  }
3840
+ if (!joining) {
3841
+ if (agentFlags.length > 0) catalogue.agents = agentFlags;
3842
+ else if (interactive) {
3843
+ const answer = await askAgents(await prompts());
3844
+ if (answer === null) return cancelled(ui);
3845
+ catalogue.agents = answer;
3846
+ }
3847
+ }
3489
3848
  if (interactive && !prompted) {
3490
3849
  const p = await prompts();
3491
3850
  const ok = await p.confirm({
@@ -3493,10 +3852,10 @@ async function createCommand(opts, deps = {}) {
3493
3852
  });
3494
3853
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
3495
3854
  }
3496
- const rnessDir = join17(root, ".rness");
3855
+ const rnessDir = join18(root, ".rness");
3497
3856
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
3498
3857
  if (joining) {
3499
- await mkdir5(root, { recursive: true });
3858
+ await mkdir6(root, { recursive: true });
3500
3859
  await placeContext(
3501
3860
  await staged(),
3502
3861
  rnessDir,
@@ -3506,7 +3865,7 @@ async function createCommand(opts, deps = {}) {
3506
3865
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3507
3866
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3508
3867
  await install();
3509
- await mkdir5(join17(root, "org"), { recursive: true });
3868
+ await mkdir6(join18(root, "org"), { recursive: true });
3510
3869
  const failures2 = await cloneSelection({
3511
3870
  root,
3512
3871
  rnessDir,
@@ -3559,7 +3918,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3559
3918
  kind: "new workspace"
3560
3919
  });
3561
3920
  if (!built) return 1;
3562
- await mkdir5(join17(root, "org"), { recursive: true });
3921
+ await mkdir6(join18(root, "org"), { recursive: true });
3563
3922
  const failures = await cloneSelection({
3564
3923
  root,
3565
3924
  rnessDir,
@@ -3660,8 +4019,8 @@ async function logoutCommand() {
3660
4019
 
3661
4020
  // src/commands/upgrade.ts
3662
4021
  import { execFile as execFile5 } from "child_process";
3663
- import { mkdir as mkdir6, readFile as readFile9, readdir as readdir3 } from "fs/promises";
3664
- import { basename as basename5, dirname as dirname7, join as join18, relative as relative5 } from "path";
4022
+ import { mkdir as mkdir7, readFile as readFile9, readdir as readdir3 } from "fs/promises";
4023
+ import { basename as basename5, dirname as dirname8, join as join19, relative as relative5 } from "path";
3665
4024
  import { promisify as promisify5 } from "util";
3666
4025
  var execFileP5 = promisify5(execFile5);
3667
4026
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -3759,15 +4118,15 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3759
4118
  return 1;
3760
4119
  }
3761
4120
  }
3762
- const packageFile = join18(ws.rnessDir, "package.json");
4121
+ const packageFile = join19(ws.rnessDir, "package.json");
3763
4122
  const restore = /* @__PURE__ */ new Map([
3764
4123
  [packageFile, await readFile9(packageFile, "utf8")]
3765
4124
  ]);
3766
4125
  if (pin.spec !== target) await writePin(ws.rnessDir, pin.spec, target);
3767
- const workflows = join18(ws.rnessDir, ".github", "workflows");
4126
+ const workflows = join19(ws.rnessDir, ".github", "workflows");
3768
4127
  for (const name of await readdir3(workflows).catch(() => [])) {
3769
4128
  if (!/\.ya?ml$/.test(name)) continue;
3770
- const file = join18(workflows, name);
4129
+ const file = join19(workflows, name);
3771
4130
  const text = await readOrNull(file);
3772
4131
  if (text === null) continue;
3773
4132
  const shown = `.github/workflows/${name}`;
@@ -3785,14 +4144,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3785
4144
  ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
3786
4145
  }
3787
4146
  }
3788
- const dependabot = join18(ws.rnessDir, ".github", "dependabot.yml");
4147
+ const dependabot = join19(ws.rnessDir, ".github", "dependabot.yml");
3789
4148
  if (await exists(dependabot)) {
3790
4149
  ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
3791
4150
  } else {
3792
- await mkdir6(dirname7(dependabot), { recursive: true });
4151
+ await mkdir7(dirname8(dependabot), { recursive: true });
3793
4152
  await writeFileAtomic(
3794
4153
  dependabot,
3795
- await readFile9(join18(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
4154
+ await readFile9(join19(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
3796
4155
  );
3797
4156
  ui.line(
3798
4157
  "wrote",
@@ -3854,7 +4213,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3854
4213
  }
3855
4214
 
3856
4215
  // src/core/blocks.ts
3857
- import { join as join19 } from "path";
4216
+ import { join as join20 } from "path";
3858
4217
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
3859
4218
  async function checkBlocks({
3860
4219
  root,
@@ -3871,14 +4230,18 @@ async function checkBlocks({
3871
4230
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
3872
4231
  checks
3873
4232
  });
3874
- if (!await exists(join19(root, "org"))) return derive();
3875
- const targets = [{ scope: null, dir: root, depth: 0, label: "AGENTS.md" }];
4233
+ if (!await exists(join20(root, "org"))) return derive();
4234
+ const targets = [
4235
+ { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4236
+ { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
4237
+ ];
3876
4238
  for (const [name, entry] of Object.entries(manifest.scopes)) {
3877
4239
  const segments = entry.path.split("/");
3878
4240
  targets.push({
3879
4241
  scope: name,
3880
- dir: join19(root, ...segments),
4242
+ dir: join20(root, ...segments),
3881
4243
  depth: segments.length,
4244
+ file: "AGENTS.md",
3882
4245
  label: `${entry.path}/AGENTS.md`
3883
4246
  });
3884
4247
  }
@@ -3891,7 +4254,7 @@ async function checkBlocks({
3891
4254
  );
3892
4255
  continue;
3893
4256
  }
3894
- const text = await readOrNull(join19(t.dir, "AGENTS.md"));
4257
+ const text = await readOrNull(join20(t.dir, t.file));
3895
4258
  const lines = text === null ? [] : text.split(/\r?\n/);
3896
4259
  const found = findBlock(lines);
3897
4260
  if (found.kind === "none") {
@@ -3917,7 +4280,8 @@ async function checkBlocks({
3917
4280
  depth: t.depth,
3918
4281
  context
3919
4282
  });
3920
- if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash))
4283
+ const pointerLeft = t.file === "CLAUDE.md" && lines.some((l) => l.trim() === "@AGENTS.md");
4284
+ if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash) && !pointerLeft)
3921
4285
  note(t.label, "current");
3922
4286
  else
3923
4287
  note(
@@ -3930,7 +4294,7 @@ async function checkBlocks({
3930
4294
  }
3931
4295
 
3932
4296
  // src/core/contract.ts
3933
- import { join as join20 } from "path";
4297
+ import { join as join21 } from "path";
3934
4298
  var STATUSES = {
3935
4299
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
3936
4300
  specs: [
@@ -3949,7 +4313,7 @@ async function checkContract(rnessDir) {
3949
4313
  const problems = [];
3950
4314
  for (const name of COLLECTIONS) {
3951
4315
  const allowed = STATUSES[name];
3952
- const items = await collectMarkdown(join20(rnessDir, name));
4316
+ const items = await collectMarkdown(join21(rnessDir, name));
3953
4317
  for (const item of items) {
3954
4318
  const where = `${name}/${item.rel}`;
3955
4319
  if (SKIP.has(where)) continue;
@@ -4001,6 +4365,16 @@ async function validateCommand(opts, deps = {}) {
4001
4365
  org
4002
4366
  });
4003
4367
  problems.push(...blocks.problems);
4368
+ for (const name of unsupportedAgents(manifest.agents ?? []))
4369
+ problems.push(unsupportedMessage(name));
4370
+ const targets = await agentTargets(ws.root, manifest, { check: true });
4371
+ for (const o of targets)
4372
+ if (o.status === "stale")
4373
+ problems.push(
4374
+ `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4375
+ );
4376
+ else if (o.status === "invalid")
4377
+ problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4004
4378
  const ui = await makeUi(deps.terminal ?? defaultTerminal);
4005
4379
  if (!ui.session) {
4006
4380
  for (const p of problems) process.stderr.write(`${p}
@@ -4012,11 +4386,19 @@ async function validateCommand(opts, deps = {}) {
4012
4386
  return 0;
4013
4387
  }
4014
4388
  ui.intro(`Validate ${org}`);
4015
- for (const c of blocks.checks)
4389
+ const rootStatus = blocks.checks.find(
4390
+ (c) => c.label === "AGENTS.md"
4391
+ )?.status;
4392
+ const shown = blocks.checks.filter(
4393
+ (c) => !(c.label === "CLAUDE.md" && c.status === rootStatus)
4394
+ );
4395
+ for (const c of shown)
4016
4396
  if (c.message === null) ui.line("current", c.label);
4017
4397
  else if (c.status === "stale" || c.status === "malformed")
4018
4398
  ui.error(c.message);
4019
4399
  else ui.warn(c.message);
4400
+ for (const o of targets)
4401
+ if (o.status === "unchanged") ui.line("current", o.label);
4020
4402
  for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
4021
4403
  ui.outro(
4022
4404
  problems.length > 0 ? "Context mismatch" : "Context OK",
@@ -4091,7 +4473,11 @@ function buildProgram(state) {
4091
4473
  ).option("--scope <name>", "only this scope (the root block is skipped)").option(
4092
4474
  "--check",
4093
4475
  "render and compare only; exit 1 when a block is out of date"
4094
- ).option("--pull", "git pull --ff-only in every clean clone").option("--all", "clone every rness.json repository missing from org/").option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
4476
+ ).option("--pull", "git pull --ff-only in every clean clone").option("--all", "clone every rness.json repository missing from org/").option(
4477
+ "--agent <name>",
4478
+ "declare an agent in rness.json, then write its files (repeatable; supported: claude)",
4479
+ (value, previous = []) => [...previous, value]
4480
+ ).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
4095
4481
  state.code = await syncCommand(opts);
4096
4482
  });
4097
4483
  program.command("add").description(
@@ -4139,6 +4525,10 @@ function buildProgram(state) {
4139
4525
  ).option("--https", "force https://github.com/").option(
4140
4526
  "--blank",
4141
4527
  "a local workspace with no GitHub organization: .rness/, an empty org/, the root files"
4528
+ ).option(
4529
+ "--agent <name>",
4530
+ "a new workspace's agent, declared in rness.json (repeatable; supported: claude)",
4531
+ (value, previous = []) => [...previous, value]
4142
4532
  ).option("--skip-install", "do not install .rness/ dependencies").option("-y, --yes", "do not ask for confirmation").option("--template <name>", "reserved").addOption(new Option("--host <base>").hideHelp()).addOption(new Option("--github-api <base>").hideHelp()).addOption(new Option("--cwd <dir>").hideHelp()).action(async (orgArg, opts) => {
4143
4533
  if (opts.blank === true) {
4144
4534
  state.code = await createCommand(
@@ -4254,6 +4644,7 @@ export {
4254
4644
  defaultTransport,
4255
4645
  findWorkspace,
4256
4646
  reportError,
4647
+ VERSION,
4257
4648
  BEGIN,
4258
4649
  END,
4259
4650
  BLOCK_SIZE_WARNING,
@@ -4275,7 +4666,6 @@ export {
4275
4666
  addCommand,
4276
4667
  renderMarkdown,
4277
4668
  contextCommand,
4278
- VERSION,
4279
4669
  findDelegate,
4280
4670
  SCAFFOLD_FILES,
4281
4671
  scaffoldDir,
@@ -4292,4 +4682,4 @@ export {
4292
4682
  OWN_COMMANDS,
4293
4683
  catchUp
4294
4684
  };
4295
- //# sourceMappingURL=chunk-YAIZOFYV.js.map
4685
+ //# sourceMappingURL=chunk-VYP4L7BQ.js.map