@rness/cli 0.6.2 → 0.8.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
@@ -794,7 +797,7 @@ function classifyProbe(url, exitCode, stderr) {
794
797
  var STDERR_FLUSH_MS = 200;
795
798
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
796
799
  positional(url, "repository url");
797
- return new Promise((resolve5) => {
800
+ return new Promise((resolve6) => {
798
801
  const auth = withCredentials(credentials);
799
802
  const child = spawn(
800
803
  "git",
@@ -813,7 +816,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
813
816
  settled = true;
814
817
  clearTimeout(timer);
815
818
  child.stderr?.destroy();
816
- resolve5(probe);
819
+ resolve6(probe);
817
820
  };
818
821
  const timer = setTimeout(() => {
819
822
  child.kill("SIGKILL");
@@ -824,14 +827,14 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
824
827
  }, timeoutMs);
825
828
  child.on("error", (e) => settle(classifyProbe(url, 1, e.message)));
826
829
  child.on("exit", (code) => {
827
- const finish = () => settle(classifyProbe(url, code ?? 1, chunks.join("")));
830
+ const finish2 = () => settle(classifyProbe(url, code ?? 1, chunks.join("")));
828
831
  const stderr = child.stderr;
829
832
  if (stderr === null || stderr.readableEnded || stderr.destroyed)
830
- return finish();
831
- const flush = setTimeout(finish, STDERR_FLUSH_MS);
833
+ return finish2();
834
+ const flush = setTimeout(finish2, STDERR_FLUSH_MS);
832
835
  const done = () => {
833
836
  clearTimeout(flush);
834
- finish();
837
+ finish2();
835
838
  };
836
839
  stderr.once("end", done);
837
840
  stderr.once("error", done);
@@ -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 = [];
@@ -1231,7 +1248,7 @@ async function detectGithubSsh(opts) {
1231
1248
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1232
1249
  "git@github.com"
1233
1250
  ];
1234
- return new Promise((resolve5) => {
1251
+ return new Promise((resolve6) => {
1235
1252
  const child = spawn2(opts.command ?? "ssh", args, {
1236
1253
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1237
1254
  });
@@ -1244,7 +1261,7 @@ async function detectGithubSsh(opts) {
1244
1261
  settled = true;
1245
1262
  clearTimeout(timer);
1246
1263
  child.stderr?.destroy();
1247
- resolve5(access2);
1264
+ resolve6(access2);
1248
1265
  };
1249
1266
  const timer = setTimeout(() => {
1250
1267
  child.kill("SIGKILL");
@@ -1252,7 +1269,7 @@ async function detectGithubSsh(opts) {
1252
1269
  }, timeoutMs);
1253
1270
  child.on("error", (e) => settle({ ok: false, reason: e.message }));
1254
1271
  child.on("exit", (code) => {
1255
- const finish = () => {
1272
+ const finish2 = () => {
1256
1273
  const stderr2 = chunks.join("");
1257
1274
  const login = parseSshGreeting(stderr2);
1258
1275
  settle(
@@ -1264,11 +1281,11 @@ async function detectGithubSsh(opts) {
1264
1281
  };
1265
1282
  const stderr = child.stderr;
1266
1283
  if (stderr === null || stderr.readableEnded || stderr.destroyed)
1267
- return finish();
1268
- const flush = setTimeout(finish, STDERR_FLUSH_MS2);
1284
+ return finish2();
1285
+ const flush = setTimeout(finish2, STDERR_FLUSH_MS2);
1269
1286
  const done = () => {
1270
1287
  clearTimeout(flush);
1271
- finish();
1288
+ finish2();
1272
1289
  };
1273
1290
  stderr.once("end", done);
1274
1291
  stderr.once("error", done);
@@ -1372,6 +1389,9 @@ function reportError(e) {
1372
1389
  return 1;
1373
1390
  }
1374
1391
 
1392
+ // src/version.ts
1393
+ var VERSION = "0.8.0";
1394
+
1375
1395
  // src/core/block.ts
1376
1396
  import { createHash } from "crypto";
1377
1397
  var BEGIN = "<!-- BEGIN rness -->";
@@ -1664,7 +1684,214 @@ async function ensureClaudeMd(dir) {
1664
1684
  }
1665
1685
 
1666
1686
  // src/commands/sync.ts
1667
- 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
+ }
1668
1895
 
1669
1896
  // src/core/progress.ts
1670
1897
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
@@ -1803,12 +2030,12 @@ function sessionUi(p) {
1803
2030
  },
1804
2031
  async step(options, work, done) {
1805
2032
  const label = options.sentence ?? tidy(options.doing);
1806
- const finish = (result) => done?.(result) ?? null;
2033
+ const finish2 = (result) => done?.(result) ?? null;
1807
2034
  const label_ = (line) => `${line[1]} ${line[0]}`;
1808
2035
  if (options.git === true && !this.gitIsSilent) {
1809
2036
  p.log.message(paint("idle", `${label}\u2026`));
1810
2037
  const result = await work();
1811
- const line = finish(result);
2038
+ const line = finish2(result);
1812
2039
  if (line !== null) say(line[0], label_(line));
1813
2040
  return result;
1814
2041
  }
@@ -1816,7 +2043,7 @@ function sessionUi(p) {
1816
2043
  spin.start(label);
1817
2044
  try {
1818
2045
  const result = await work();
1819
- const line = finish(result);
2046
+ const line = finish2(result);
1820
2047
  spin.stop(line === null ? label : label_(line));
1821
2048
  return result;
1822
2049
  } catch (e) {
@@ -1853,7 +2080,7 @@ function targetsOf(manifest, root, only) {
1853
2080
  const segments = entry.path.split("/");
1854
2081
  targets.push({
1855
2082
  scope: name,
1856
- dir: join9(root, ...segments),
2083
+ dir: join10(root, ...segments),
1857
2084
  depth: segments.length,
1858
2085
  label: `${entry.path}/AGENTS.md`
1859
2086
  });
@@ -1917,16 +2144,60 @@ async function syncCommand(opts, deps = {}) {
1917
2144
  const check = opts.check === true;
1918
2145
  const ownUi = deps.ui === void 0;
1919
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
+ }
1920
2162
  try {
1921
2163
  const ws = await findWorkspace(cwd);
1922
2164
  const manifest = await loadManifest(ws.rnessDir);
1923
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
+ }
1924
2172
  if (ownUi) ui.intro(`Sync workspace ${org}`);
1925
2173
  if (opts.scope !== void 0 && !Object.hasOwn(manifest.scopes, opts.scope)) {
1926
2174
  process.stderr.write(`unknown scope: ${opts.scope}
1927
2175
  `);
1928
2176
  return 1;
1929
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
+ }
1930
2201
  const clones = await clonesIn(ws.root);
1931
2202
  const missing = Object.keys(manifest.repos).filter((n) => !clones.has(n));
1932
2203
  let toClone = new Set(opts.all === true && !check ? missing : []);
@@ -1968,7 +2239,7 @@ async function syncCommand(opts, deps = {}) {
1968
2239
  try {
1969
2240
  const notCloned = /* @__PURE__ */ new Set();
1970
2241
  for (const [name, repo] of Object.entries(manifest.repos)) {
1971
- const dir = join9(ws.root, "org", name);
2242
+ const dir = join10(ws.root, "org", name);
1972
2243
  const label = `org/${name}`;
1973
2244
  if (!await exists(dir)) {
1974
2245
  if (!toClone.has(name)) {
@@ -1989,7 +2260,13 @@ async function syncCommand(opts, deps = {}) {
1989
2260
  }
1990
2261
  } else if (opts.pull === true && !check) {
1991
2262
  try {
1992
- 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)) {
1993
2270
  lines.push(["skipped", `${label} (working tree not clean)`]);
1994
2271
  continue;
1995
2272
  }
@@ -2020,7 +2297,7 @@ async function syncCommand(opts, deps = {}) {
2020
2297
  lines.push(
2021
2298
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
2022
2299
  );
2023
- if (!await exists(join9(ws.root, "org"))) {
2300
+ if (!await exists(join10(ws.root, "org"))) {
2024
2301
  lines.push(["skipped", "blocks (no org/ directory here)"]);
2025
2302
  } else {
2026
2303
  let differences = 0;
@@ -2032,7 +2309,7 @@ async function syncCommand(opts, deps = {}) {
2032
2309
  continue;
2033
2310
  }
2034
2311
  const [agentsLinked, claudeLinked] = await Promise.all(
2035
- MANAGED_FILES.map((f) => isSymlink(join9(t.dir, f)))
2312
+ MANAGED_FILES.map((f) => isSymlink(join10(t.dir, f)))
2036
2313
  );
2037
2314
  const atRoot = t.scope === null;
2038
2315
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -2055,7 +2332,7 @@ async function syncCommand(opts, deps = {}) {
2055
2332
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
2056
2333
  );
2057
2334
  }
2058
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join9(t.dir, "AGENTS.md"), block, check);
2335
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join10(t.dir, "AGENTS.md"), block, check);
2059
2336
  if (typeof agents === "object") {
2060
2337
  problems.push(`${t.label}: ${agents.error}`);
2061
2338
  continue;
@@ -2070,7 +2347,7 @@ async function syncCommand(opts, deps = {}) {
2070
2347
  }
2071
2348
  if (claudeLinked === true) continue;
2072
2349
  const claude = await syncBlockFile(
2073
- join9(t.dir, "CLAUDE.md"),
2350
+ join10(t.dir, "CLAUDE.md"),
2074
2351
  block,
2075
2352
  check,
2076
2353
  true
@@ -2086,6 +2363,28 @@ async function syncCommand(opts, deps = {}) {
2086
2363
  problems.push(
2087
2364
  `${differences} block(s) out of date \u2014 run ${rnessCommand()} sync`
2088
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
+ }
2089
2388
  }
2090
2389
  return problems.length > 0 ? 1 : 0;
2091
2390
  } finally {
@@ -2105,7 +2404,7 @@ async function syncCommand(opts, deps = {}) {
2105
2404
  }
2106
2405
 
2107
2406
  // src/commands/add.ts
2108
- import { basename as basename3, join as join10 } from "path";
2407
+ import { basename as basename3, join as join11 } from "path";
2109
2408
  function printDeclared(ui, result, already) {
2110
2409
  for (const name of result.declared) {
2111
2410
  if (already.has(name)) continue;
@@ -2198,7 +2497,7 @@ async function addCommand(spec, opts, deps = {}) {
2198
2497
  }
2199
2498
  parsed = parseRepoSpec(spec, org, host);
2200
2499
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
2201
- const dir = join10(ws.root, "org", parsed.name);
2500
+ const dir = join11(ws.root, "org", parsed.name);
2202
2501
  const present = await exists(dir);
2203
2502
  if (interactive) {
2204
2503
  const { confirm, isCancel } = await terminal.prompts();
@@ -2324,26 +2623,23 @@ async function contextCommand(opts) {
2324
2623
  }
2325
2624
  }
2326
2625
 
2327
- // src/version.ts
2328
- var VERSION = "0.6.2";
2329
-
2330
2626
  // src/core/delegate.ts
2331
2627
  import { readFile as readFile6 } from "fs/promises";
2332
- import { join as join11 } from "path";
2628
+ import { join as join12 } from "path";
2333
2629
  var DEFAULT_BIN = "dist/bin/rness.js";
2334
2630
  async function readPinnedCli(rnessDir) {
2335
- const dir = join11(rnessDir, "node_modules", "@rness", "cli");
2631
+ const dir = join12(rnessDir, "node_modules", "@rness", "cli");
2336
2632
  let pkg;
2337
2633
  try {
2338
- pkg = JSON.parse(await readFile6(join11(dir, "package.json"), "utf8"));
2634
+ pkg = JSON.parse(await readFile6(join12(dir, "package.json"), "utf8"));
2339
2635
  } catch {
2340
2636
  return null;
2341
2637
  }
2342
- const declared = pkg.bin;
2638
+ const declared2 = pkg.bin;
2343
2639
  let bin = DEFAULT_BIN;
2344
- if (typeof declared === "string") bin = declared;
2345
- else if (declared !== null && typeof declared === "object") {
2346
- const named = declared["rness"];
2640
+ if (typeof declared2 === "string") bin = declared2;
2641
+ else if (declared2 !== null && typeof declared2 === "object") {
2642
+ const named = declared2["rness"];
2347
2643
  if (typeof named === "string") bin = named;
2348
2644
  }
2349
2645
  return {
@@ -2355,7 +2651,7 @@ async function readPinnedCli(rnessDir) {
2355
2651
  async function pinnedBinPath(rnessDir) {
2356
2652
  const pinned = await readPinnedCli(rnessDir);
2357
2653
  if (pinned === null) return null;
2358
- return join11(pinned.dir, ...pinned.bin.split("/"));
2654
+ return join12(pinned.dir, ...pinned.bin.split("/"));
2359
2655
  }
2360
2656
  async function findDelegate(cwd, ownVersion) {
2361
2657
  let root;
@@ -2371,12 +2667,12 @@ async function findDelegate(cwd, ownVersion) {
2371
2667
  if (pinned === null) return null;
2372
2668
  const { version } = pinned;
2373
2669
  if (version === null || version === ownVersion) return null;
2374
- return { entry: join11(pinned.dir, "dist", "index.js"), version, root };
2670
+ return { entry: join12(pinned.dir, "dist", "index.js"), version, root };
2375
2671
  }
2376
2672
 
2377
2673
  // src/core/scaffold.ts
2378
2674
  import { statSync } from "fs";
2379
- import { dirname as dirname4, join as join12 } from "path";
2675
+ import { dirname as dirname5, join as join13 } from "path";
2380
2676
  import { fileURLToPath } from "url";
2381
2677
  var SCAFFOLD_FILES = [
2382
2678
  "AGENTS.md",
@@ -2398,37 +2694,45 @@ var SCAFFOLD_FILES = [
2398
2694
  "docs/.gitkeep"
2399
2695
  ];
2400
2696
  function scaffoldDir() {
2401
- let dir = dirname4(fileURLToPath(import.meta.url));
2697
+ let dir = dirname5(fileURLToPath(import.meta.url));
2402
2698
  for (let i = 0; i < 4; i += 1) {
2403
- const candidate = join12(dir, "scaffold");
2699
+ const candidate = join13(dir, "scaffold");
2404
2700
  try {
2405
- if (statSync(join12(candidate, "rness.json")).isFile()) return candidate;
2701
+ if (statSync(join13(candidate, "rness.json")).isFile()) return candidate;
2406
2702
  } catch {
2407
2703
  }
2408
- dir = dirname4(dir);
2704
+ dir = dirname5(dir);
2409
2705
  }
2410
2706
  throw new Error("scaffold directory not found next to the @rness/cli package");
2411
2707
  }
2412
2708
 
2413
2709
  // src/core/scaffold-copy.ts
2414
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2415
- 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";
2416
2712
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
2417
- async function copyScaffold(dest, tokens) {
2418
- const source = scaffoldDir();
2713
+ async function renderScaffold(source, tokens) {
2714
+ const files = [];
2419
2715
  for (const rel of SCAFFOLD_FILES) {
2420
2716
  if (rel === "rness.json") continue;
2421
- const from = join13(source, ...rel.split("/"));
2422
- const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
2423
- const to = join13(dest, ...targetRel.split("/"));
2424
- await mkdir2(dirname5(to), { recursive: true });
2717
+ const from = join14(source, ...rel.split("/"));
2425
2718
  let content = await readFile7(from, "utf8");
2426
2719
  if (TOKENISED.has(rel)) {
2427
2720
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
2428
2721
  }
2429
- await writeFile2(to, content, "utf8");
2430
- const mode = (await stat2(from)).mode & 511;
2431
- if (mode & 73) await chmod2(to, mode);
2722
+ files.push({
2723
+ path: rel === "_gitignore" ? ".gitignore" : rel,
2724
+ content,
2725
+ executable: ((await stat2(from)).mode & 73) !== 0
2726
+ });
2727
+ }
2728
+ return files;
2729
+ }
2730
+ async function copyScaffold(dest, tokens) {
2731
+ for (const file of await renderScaffold(scaffoldDir(), tokens)) {
2732
+ const to = join14(dest, ...file.path.split("/"));
2733
+ await mkdir3(dirname6(to), { recursive: true });
2734
+ await writeFile2(to, file.content, "utf8");
2735
+ if (file.executable) await chmod2(to, 493);
2432
2736
  }
2433
2737
  }
2434
2738
 
@@ -2582,18 +2886,18 @@ function revokeUrl() {
2582
2886
  }
2583
2887
 
2584
2888
  // src/commands/create.ts
2585
- import { mkdir as mkdir5, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
2586
- import { tmpdir } from "os";
2587
- import { join as join17, relative as relative4, resolve as resolve4 } from "path";
2889
+ import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
2890
+ import { tmpdir as tmpdir2 } from "os";
2891
+ import { join as join19, relative as relative4, resolve as resolve5 } from "path";
2588
2892
 
2589
2893
  // src/core/new-workspace.ts
2590
- import { mkdir as mkdir3, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
2591
- import { dirname as dirname6, join as join15, relative as relative2 } from "path";
2894
+ import { mkdir as mkdir4, readdir as readdir2, rm as rm5, stat as stat3 } from "fs/promises";
2895
+ import { dirname as dirname8, join as join17, relative as relative2 } from "path";
2592
2896
 
2593
2897
  // src/core/pinned.ts
2594
2898
  import { execFile as execFile4 } from "child_process";
2595
2899
  import { readFile as readFile8 } from "fs/promises";
2596
- import { basename as basename4, join as join14 } from "path";
2900
+ import { basename as basename4, join as join15 } from "path";
2597
2901
  import { promisify as promisify4 } from "util";
2598
2902
  var execFileP4 = promisify4(execFile4);
2599
2903
  var PACKAGE = "@rness/cli";
@@ -2618,7 +2922,7 @@ function compareVersions(a, b) {
2618
2922
  async function readPackageJson(rnessDir) {
2619
2923
  try {
2620
2924
  const parsed = JSON.parse(
2621
- await readFile8(join14(rnessDir, "package.json"), "utf8")
2925
+ await readFile8(join15(rnessDir, "package.json"), "utf8")
2622
2926
  );
2623
2927
  return parsed !== null && typeof parsed === "object" ? parsed : null;
2624
2928
  } catch {
@@ -2637,7 +2941,7 @@ async function readPin(rnessDir) {
2637
2941
  return null;
2638
2942
  }
2639
2943
  async function writePin(rnessDir, from, to) {
2640
- const file = join14(rnessDir, "package.json");
2944
+ const file = join15(rnessDir, "package.json");
2641
2945
  const text = await readFile8(file, "utf8");
2642
2946
  const entry = new RegExp(
2643
2947
  `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
@@ -2654,18 +2958,18 @@ var LOCKFILES = [
2654
2958
  ["package-lock.json", "npm"]
2655
2959
  ];
2656
2960
  async function workspacePackageManager(rnessDir) {
2657
- const declared = (await readPackageJson(rnessDir))?.["packageManager"];
2658
- if (typeof declared === "string") {
2659
- const name = declared.split("@")[0] ?? "";
2961
+ const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
2962
+ if (typeof declared2 === "string") {
2963
+ const name = declared2.split("@")[0] ?? "";
2660
2964
  if (isPackageManager(name)) return name;
2661
2965
  }
2662
2966
  for (const [lockfile, pm] of LOCKFILES)
2663
- if (await exists(join14(rnessDir, lockfile))) return pm;
2967
+ if (await exists(join15(rnessDir, lockfile))) return pm;
2664
2968
  return "npm";
2665
2969
  }
2666
2970
  async function hasLockfile(rnessDir, pm) {
2667
2971
  for (const [lockfile, owner] of LOCKFILES)
2668
- if (owner === pm && await exists(join14(rnessDir, lockfile))) return true;
2972
+ if (owner === pm && await exists(join15(rnessDir, lockfile))) return true;
2669
2973
  return false;
2670
2974
  }
2671
2975
  async function pinDrift(rnessDir) {
@@ -2721,9 +3025,9 @@ async function ensureInstalled(rnessDir, opts = {}) {
2721
3025
  }
2722
3026
  }
2723
3027
  async function runPinnedSync(root) {
2724
- const installed = await readPinnedCli(join14(root, ".rness"));
3028
+ const installed = await readPinnedCli(join15(root, ".rness"));
2725
3029
  if (installed === null) return null;
2726
- const pinned = join14(installed.dir, ...installed.bin.split("/"));
3030
+ const pinned = join15(installed.dir, ...installed.bin.split("/"));
2727
3031
  if (!await exists(pinned))
2728
3032
  return {
2729
3033
  code: 1,
@@ -2785,7 +3089,7 @@ function syncOutcome(result, opts = {}) {
2785
3089
  };
2786
3090
  }
2787
3091
  async function syncPinned(root, shown, fallback) {
2788
- const rnessDir = join14(root, ".rness");
3092
+ const rnessDir = join15(root, ".rness");
2789
3093
  const installed = await readPinnedCli(rnessDir);
2790
3094
  if (installed === null) {
2791
3095
  if (fallback !== void 0) return fallback();
@@ -2795,7 +3099,7 @@ async function syncPinned(root, shown, fallback) {
2795
3099
  );
2796
3100
  return 1;
2797
3101
  }
2798
- const pinned = join14(installed.dir, ...installed.bin.split("/"));
3102
+ const pinned = join15(installed.dir, ...installed.bin.split("/"));
2799
3103
  if (!await exists(pinned)) {
2800
3104
  process.stderr.write(
2801
3105
  `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
@@ -2828,6 +3132,166 @@ async function syncPinned(root, shown, fallback) {
2828
3132
  }
2829
3133
  }
2830
3134
 
3135
+ // src/core/scaffold-git.ts
3136
+ import { execFile as execFile5 } from "child_process";
3137
+ import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3138
+ import { tmpdir } from "os";
3139
+ import { dirname as dirname7, join as join16, resolve as resolve3 } from "path";
3140
+ import { promisify as promisify5 } from "util";
3141
+ var execFileP5 = promisify5(execFile5);
3142
+ var CREATE_SUBJECT = "chore: rness workspace context";
3143
+ var SCAFFOLD_SUBJECT = /^chore: rness scaffold (\S+)$/;
3144
+ var SCAFFOLD_TRAILER = "Rness-Scaffold";
3145
+ async function run(dir, args, env = {}) {
3146
+ try {
3147
+ const { stdout, stderr } = await execFileP5("git", [...args], {
3148
+ cwd: dir,
3149
+ env: {
3150
+ ...process.env,
3151
+ ...env,
3152
+ GIT_TERMINAL_PROMPT: "0",
3153
+ GIT_CEILING_DIRECTORIES: dirname7(resolve3(dir))
3154
+ },
3155
+ maxBuffer: 16 * 1024 * 1024
3156
+ });
3157
+ return { code: 0, stdout, stderr };
3158
+ } catch (e) {
3159
+ const err = e;
3160
+ return {
3161
+ code: typeof err.code === "number" ? err.code : 1,
3162
+ stdout: err.stdout ?? "",
3163
+ stderr: err.stderr ?? String(e)
3164
+ };
3165
+ }
3166
+ }
3167
+ async function must(dir, args, env = {}) {
3168
+ const r = await run(dir, args, env);
3169
+ if (r.code !== 0) {
3170
+ const first = r.stderr.split("\n").find((l) => l.trim() !== "") ?? "unknown error";
3171
+ throw new Error(`git ${args[0] ?? ""} failed: ${first.trim()}`);
3172
+ }
3173
+ return r.stdout.trim();
3174
+ }
3175
+ function scaffoldMessage(subject, version) {
3176
+ return `${subject}
3177
+
3178
+ ${SCAFFOLD_TRAILER}: ${version}`;
3179
+ }
3180
+ async function findScaffoldBase(rnessDir) {
3181
+ const log = await run(rnessDir, [
3182
+ "log",
3183
+ "--topo-order",
3184
+ `--format=%H%x1f%P%x1f%s%x1f%(trailers:key=${SCAFFOLD_TRAILER},valueonly,separator=%x2C)%x1e`,
3185
+ "HEAD"
3186
+ ]);
3187
+ if (log.code !== 0) return null;
3188
+ const commits = log.stdout.split("").map((r) => r.trim()).filter((r) => r !== "").map((r) => {
3189
+ const [hash = "", parents = "", subject = "", trailer = ""] = r.split("");
3190
+ return { hash, parents: parents.trim(), subject, trailer: trailer.trim() };
3191
+ });
3192
+ for (const c of commits) {
3193
+ const m = SCAFFOLD_SUBJECT.exec(c.subject);
3194
+ if (m !== null) return { commit: c.hash, version: m[1] ?? null };
3195
+ }
3196
+ const root = commits.find(
3197
+ (c) => c.parents === "" && c.subject === CREATE_SUBJECT
3198
+ );
3199
+ if (root === void 0) return null;
3200
+ return {
3201
+ commit: root.hash,
3202
+ version: root.trailer === "" ? null : root.trailer
3203
+ };
3204
+ }
3205
+ async function buildScaffoldCommit(rnessDir, base, files, version) {
3206
+ const scratch = await mkdtemp(join16(tmpdir(), "rness-scaffold-"));
3207
+ const env = { GIT_INDEX_FILE: join16(scratch, "index") };
3208
+ try {
3209
+ await must(
3210
+ rnessDir,
3211
+ ["read-tree", ...base === null ? ["--empty"] : [base]],
3212
+ env
3213
+ );
3214
+ for (const [i, file] of files.entries()) {
3215
+ const tmp = join16(scratch, `blob-${i}`);
3216
+ await writeFile3(tmp, file.content, "utf8");
3217
+ const blob = await must(rnessDir, [
3218
+ "hash-object",
3219
+ "-w",
3220
+ "--no-filters",
3221
+ "--",
3222
+ tmp
3223
+ ]);
3224
+ await must(
3225
+ rnessDir,
3226
+ [
3227
+ "update-index",
3228
+ "--add",
3229
+ "--cacheinfo",
3230
+ `${file.executable ? "100755" : "100644"},${blob},${file.path}`
3231
+ ],
3232
+ env
3233
+ );
3234
+ }
3235
+ const tree = await must(rnessDir, ["write-tree"], env);
3236
+ return await must(rnessDir, [
3237
+ "commit-tree",
3238
+ tree,
3239
+ ...base === null ? [] : ["-p", base],
3240
+ "-m",
3241
+ scaffoldMessage(`chore: rness scaffold ${version}`, version)
3242
+ ]);
3243
+ } finally {
3244
+ await rm4(scratch, { recursive: true, force: true });
3245
+ }
3246
+ }
3247
+ async function blobAt(rnessDir, rev, path) {
3248
+ const r = await run(rnessDir, [
3249
+ "rev-parse",
3250
+ "--verify",
3251
+ "--quiet",
3252
+ `${rev}:${path}`
3253
+ ]);
3254
+ return r.code === 0 ? r.stdout.trim() : null;
3255
+ }
3256
+ async function mergeScaffold(rnessDir, commit, base) {
3257
+ await run(rnessDir, [
3258
+ "merge",
3259
+ "--no-commit",
3260
+ "--no-ff",
3261
+ ...base === null ? ["--allow-unrelated-histories"] : [],
3262
+ commit
3263
+ ]);
3264
+ const conflicts = (await must(rnessDir, ["diff", "--name-only", "--diff-filter=U"])).split("\n").filter((l) => l !== "");
3265
+ const changes = [];
3266
+ const status2 = await must(rnessDir, [
3267
+ "diff",
3268
+ "--cached",
3269
+ "--name-status",
3270
+ "HEAD"
3271
+ ]);
3272
+ for (const line of status2.split("\n")) {
3273
+ const [letter = "", path = ""] = line.split(" ");
3274
+ if (path === "" || conflicts.includes(path)) continue;
3275
+ if (letter === "A") changes.push(["added", path]);
3276
+ else if (letter === "D") changes.push(["removed", path]);
3277
+ else if (letter === "M") {
3278
+ const untouched = base !== null && await blobAt(rnessDir, "HEAD", path) === await blobAt(rnessDir, base, path);
3279
+ changes.push([untouched ? "updated" : "merged", path]);
3280
+ }
3281
+ }
3282
+ return { conflicts, changes };
3283
+ }
3284
+ async function isShallow(rnessDir) {
3285
+ const r = await run(rnessDir, ["rev-parse", "--is-shallow-repository"]);
3286
+ return r.code === 0 && r.stdout.trim() === "true";
3287
+ }
3288
+ async function isRepository2(rnessDir) {
3289
+ const r = await run(rnessDir, ["rev-parse", "--show-toplevel"]);
3290
+ if (r.code !== 0) return false;
3291
+ const real = (p) => realpath2(p).catch(() => resolve3(p));
3292
+ return await real(r.stdout.trim()) === await real(rnessDir);
3293
+ }
3294
+
2831
3295
  // src/core/new-workspace.ts
2832
3296
  async function isEmptyDir(dir) {
2833
3297
  return (await readdir2(dir)).length === 0;
@@ -2842,11 +3306,11 @@ async function insideWorkspace(dir) {
2842
3306
  }
2843
3307
  async function targetProblem(cwd, root, pm) {
2844
3308
  const shown = relative2(cwd, root) || ".";
2845
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname6(root)))
3309
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
2846
3310
  return "already inside an rness workspace; create runs from outside one";
2847
3311
  if (!await exists(root)) return null;
2848
- if (await exists(join15(root, ".rness", "rness.json"))) {
2849
- if (!await exists(join15(root, ".rness", "node_modules")))
3312
+ if (await exists(join17(root, ".rness", "rness.json"))) {
3313
+ if (!await exists(join17(root, ".rness", "node_modules")))
2850
3314
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
2851
3315
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
2852
3316
  }
@@ -2869,9 +3333,9 @@ async function installContext(input) {
2869
3333
  }
2870
3334
  async function buildContext(input) {
2871
3335
  const { ui, root, shown, pm } = input;
2872
- const rnessDir = join15(root, ".rness");
3336
+ const rnessDir = join17(root, ".rness");
2873
3337
  const pmVersion = await packageManagerVersion(pm);
2874
- await mkdir3(root, { recursive: true });
3338
+ await mkdir4(root, { recursive: true });
2875
3339
  try {
2876
3340
  await copyScaffold(rnessDir, {
2877
3341
  version: VERSION,
@@ -2883,9 +3347,9 @@ async function buildContext(input) {
2883
3347
  return true;
2884
3348
  } catch (e) {
2885
3349
  reportError(e);
2886
- await rm4(rnessDir, { recursive: true, force: true }).catch(() => void 0);
3350
+ await rm5(rnessDir, { recursive: true, force: true }).catch(() => void 0);
2887
3351
  if (await isEmptyDir(root).catch(() => false))
2888
- await rm4(root, { recursive: true, force: true }).catch(() => void 0);
3352
+ await rm5(root, { recursive: true, force: true }).catch(() => void 0);
2889
3353
  process.stderr.write(
2890
3354
  `removed ${shown}/.rness after the failure; fix it and retry
2891
3355
  `
@@ -2897,7 +3361,7 @@ async function commitContext(input) {
2897
3361
  const { ui, rnessDir, shown } = input;
2898
3362
  await init(rnessDir);
2899
3363
  try {
2900
- await commitAll(rnessDir, "chore: rness workspace context");
3364
+ await commitAll(rnessDir, scaffoldMessage(CREATE_SUBJECT, VERSION));
2901
3365
  ui.line("committed", `${shown}/.rness`);
2902
3366
  } catch (e) {
2903
3367
  ui.warn(
@@ -2937,8 +3401,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
2937
3401
  }
2938
3402
 
2939
3403
  // src/commands/create-blank.ts
2940
- import { mkdir as mkdir4 } from "fs/promises";
2941
- import { join as join16, relative as relative3, resolve as resolve3 } from "path";
3404
+ import { mkdir as mkdir5 } from "fs/promises";
3405
+ import { join as join18, relative as relative3, resolve as resolve4 } from "path";
2942
3406
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
2943
3407
  function workspaceNameError(name) {
2944
3408
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -2970,7 +3434,7 @@ async function createBlank(input) {
2970
3434
  name = answer.trim();
2971
3435
  prompted = true;
2972
3436
  }
2973
- const root = resolve3(cwd, name);
3437
+ const root = resolve4(cwd, name);
2974
3438
  const shown = relative3(cwd, root) || ".";
2975
3439
  const problem = await targetProblem(cwd, root, pm);
2976
3440
  if (problem !== null) {
@@ -2984,6 +3448,14 @@ async function createBlank(input) {
2984
3448
  );
2985
3449
  return 2;
2986
3450
  }
3451
+ let agents = input.agents.length > 0 ? [...input.agents] : null;
3452
+ if (agents === null && input.interactive) {
3453
+ agents = await askAgents(await input.prompts());
3454
+ if (agents === null) {
3455
+ ui.cancelled();
3456
+ return 0;
3457
+ }
3458
+ }
2987
3459
  if (input.interactive && !prompted) {
2988
3460
  const p = await input.prompts();
2989
3461
  const ok = await p.confirm({
@@ -2998,14 +3470,14 @@ async function createBlank(input) {
2998
3470
  ui,
2999
3471
  root,
3000
3472
  shown,
3001
- manifest: { contract: 1, org: null, repos: {}, scopes: {} },
3473
+ manifest: { contract: 1, org: null, agents, repos: {}, scopes: {} },
3002
3474
  pm,
3003
3475
  skipInstall: input.skipInstall,
3004
3476
  kind: "blank workspace"
3005
3477
  });
3006
3478
  if (!built) return 1;
3007
- await mkdir4(join16(root, "org"), { recursive: true });
3008
- await commitContext({ ui, rnessDir: join16(root, ".rness"), shown });
3479
+ await mkdir5(join18(root, "org"), { recursive: true });
3480
+ await commitContext({ ui, rnessDir: join18(root, ".rness"), shown });
3009
3481
  const code = await syncBlocks(
3010
3482
  ui,
3011
3483
  root,
@@ -3165,7 +3637,7 @@ async function cloneSelection(input) {
3165
3637
  { doing: `cloning org/${repo}`, git: true },
3166
3638
  () => clone(
3167
3639
  entry.url,
3168
- join17(input.root, "org", repo),
3640
+ join19(input.root, "org", repo),
3169
3641
  input.provider.credentialsFor(entry.url)
3170
3642
  ),
3171
3643
  () => ["cloned", `org/${repo}`]
@@ -3296,6 +3768,15 @@ async function createCommand(opts, deps = {}) {
3296
3768
  `);
3297
3769
  return 2;
3298
3770
  }
3771
+ const agentFlags = [...new Set(opts.agent ?? [])];
3772
+ const unknownAgent = unsupportedAgents(agentFlags)[0];
3773
+ if (unknownAgent !== void 0) {
3774
+ process.stderr.write(
3775
+ `unknown agent "${unknownAgent}" (supported: ${SUPPORTED_AGENTS.join(", ")})
3776
+ `
3777
+ );
3778
+ return 2;
3779
+ }
3299
3780
  if (opts.ssh === true && opts.https === true) {
3300
3781
  process.stderr.write("--ssh and --https cannot be combined\n");
3301
3782
  return 2;
@@ -3369,6 +3850,7 @@ async function createCommand(opts, deps = {}) {
3369
3850
  if (blank)
3370
3851
  return await createBlank({
3371
3852
  name: opts.name,
3853
+ agents: agentFlags,
3372
3854
  cwd,
3373
3855
  pm,
3374
3856
  interactive,
@@ -3424,7 +3906,7 @@ async function createCommand(opts, deps = {}) {
3424
3906
  org = answer.trim();
3425
3907
  prompted = true;
3426
3908
  }
3427
- const root = resolve4(cwd, org);
3909
+ const root = resolve5(cwd, org);
3428
3910
  const shown = relative4(cwd, root) || ".";
3429
3911
  const problem = await targetProblem(cwd, root, pm);
3430
3912
  if (problem !== null) {
@@ -3452,6 +3934,13 @@ async function createCommand(opts, deps = {}) {
3452
3934
  return 1;
3453
3935
  }
3454
3936
  const joining = probe.kind === "found";
3937
+ if (joining && agentFlags.length > 0) {
3938
+ process.stderr.write(
3939
+ `${org}/.rness exists: --agent only applies to a new workspace; after joining, run ${rnessCommand()} sync${agentFlags.map((a) => ` --agent ${a}`).join("")}
3940
+ `
3941
+ );
3942
+ return 2;
3943
+ }
3455
3944
  if (joining)
3456
3945
  ui.line(
3457
3946
  "found",
@@ -3464,10 +3953,16 @@ async function createCommand(opts, deps = {}) {
3464
3953
  `${org}/.rness (or not visible to you) \u2014 starting a new workspace`,
3465
3954
  `${org} has no workspace yet (or none you can see) \u2014 starting one`
3466
3955
  );
3467
- let catalogue = { contract: 1, org, repos: {}, scopes: {} };
3956
+ let catalogue = {
3957
+ contract: 1,
3958
+ org,
3959
+ agents: null,
3960
+ repos: {},
3961
+ scopes: {}
3962
+ };
3468
3963
  const staged = async () => {
3469
- staging ??= await mkdtemp(join17(tmpdir(), "rness-join-"));
3470
- return join17(staging, ".rness");
3964
+ staging ??= await mkdtemp2(join19(tmpdir2(), "rness-join-"));
3965
+ return join19(staging, ".rness");
3471
3966
  };
3472
3967
  if (joining) {
3473
3968
  const stagedDir = await staged();
@@ -3510,6 +4005,14 @@ async function createCommand(opts, deps = {}) {
3510
4005
  } else {
3511
4006
  specs = Object.keys(catalogue.repos);
3512
4007
  }
4008
+ if (!joining) {
4009
+ if (agentFlags.length > 0) catalogue.agents = agentFlags;
4010
+ else if (interactive) {
4011
+ const answer = await askAgents(await prompts());
4012
+ if (answer === null) return cancelled(ui);
4013
+ catalogue.agents = answer;
4014
+ }
4015
+ }
3513
4016
  if (interactive && !prompted) {
3514
4017
  const p = await prompts();
3515
4018
  const ok = await p.confirm({
@@ -3517,10 +4020,10 @@ async function createCommand(opts, deps = {}) {
3517
4020
  });
3518
4021
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
3519
4022
  }
3520
- const rnessDir = join17(root, ".rness");
4023
+ const rnessDir = join19(root, ".rness");
3521
4024
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
3522
4025
  if (joining) {
3523
- await mkdir5(root, { recursive: true });
4026
+ await mkdir6(root, { recursive: true });
3524
4027
  await placeContext(
3525
4028
  await staged(),
3526
4029
  rnessDir,
@@ -3530,7 +4033,7 @@ async function createCommand(opts, deps = {}) {
3530
4033
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3531
4034
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3532
4035
  await install();
3533
- await mkdir5(join17(root, "org"), { recursive: true });
4036
+ await mkdir6(join19(root, "org"), { recursive: true });
3534
4037
  const failures2 = await cloneSelection({
3535
4038
  root,
3536
4039
  rnessDir,
@@ -3583,7 +4086,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3583
4086
  kind: "new workspace"
3584
4087
  });
3585
4088
  if (!built) return 1;
3586
- await mkdir5(join17(root, "org"), { recursive: true });
4089
+ await mkdir6(join19(root, "org"), { recursive: true });
3587
4090
  const failures = await cloneSelection({
3588
4091
  root,
3589
4092
  rnessDir,
@@ -3650,7 +4153,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3650
4153
  return reportError(e);
3651
4154
  } finally {
3652
4155
  if (staging !== void 0)
3653
- await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
4156
+ await rm6(staging, { recursive: true, force: true }).catch(() => void 0);
3654
4157
  }
3655
4158
  }
3656
4159
 
@@ -3683,17 +4186,16 @@ async function logoutCommand() {
3683
4186
  }
3684
4187
 
3685
4188
  // src/commands/upgrade.ts
3686
- import { execFile as execFile5 } from "child_process";
3687
- import { mkdir as mkdir6, readFile as readFile9, readdir as readdir3 } from "fs/promises";
3688
- import { basename as basename5, dirname as dirname7, join as join18, relative as relative5 } from "path";
3689
- import { promisify as promisify5 } from "util";
3690
- var execFileP5 = promisify5(execFile5);
4189
+ import { execFile as execFile6 } from "child_process";
4190
+ import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4191
+ import { tmpdir as tmpdir3 } from "os";
4192
+ import { basename as basename5, join as join20, relative as relative5 } from "path";
4193
+ import { promisify as promisify6 } from "util";
4194
+ var execFileP6 = promisify6(execFile6);
3691
4195
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
3692
- var OLD_WORKFLOW_RUN = /npx --yes @rness\/cli@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?) validate/;
3693
- var WORKFLOW_RUN = `npx --yes "@rness/cli@$(node -p "require('./package.json').devDependencies['@rness/cli']")" validate`;
3694
4196
  async function latestVersion() {
3695
4197
  try {
3696
- const { stdout } = await execFileP5(
4198
+ const { stdout } = await execFileP6(
3697
4199
  "npm",
3698
4200
  ["view", "@rness/cli@latest", "version"],
3699
4201
  { timeout: 3e4 }
@@ -3704,7 +4206,70 @@ async function latestVersion() {
3704
4206
  return null;
3705
4207
  }
3706
4208
  }
3707
- async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4209
+ async function scaffoldFor(version, rnessDir) {
4210
+ if (version === VERSION) return { dir: scaffoldDir() };
4211
+ const installed = await readPinnedCli(rnessDir);
4212
+ const inside = installed === null ? null : join20(installed.dir, "scaffold");
4213
+ if (installed?.version === version && inside !== null && await exists(join20(inside, "package.json")))
4214
+ return { dir: inside };
4215
+ const scratch = await mkdtemp3(join20(tmpdir3(), "rness-pack-"));
4216
+ const cleanup = () => rm7(scratch, { recursive: true, force: true });
4217
+ try {
4218
+ const { stdout } = await execFileP6(
4219
+ "npm",
4220
+ [
4221
+ "pack",
4222
+ `@rness/cli@${version}`,
4223
+ "--pack-destination",
4224
+ scratch,
4225
+ "--silent"
4226
+ ],
4227
+ { timeout: 6e4 }
4228
+ );
4229
+ const tarball = stdout.trim().split("\n").at(-1) ?? "";
4230
+ await execFileP6("tar", ["-xzf", join20(scratch, tarball), "-C", scratch]);
4231
+ return { dir: join20(scratch, "package", "scaffold"), cleanup };
4232
+ } catch (e) {
4233
+ await cleanup();
4234
+ throw new Error(
4235
+ `cannot get the @rness/cli ${version} scaffold (npm pack): ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`,
4236
+ { cause: e }
4237
+ );
4238
+ }
4239
+ }
4240
+ async function declaredManager(rnessDir, pm) {
4241
+ try {
4242
+ const pkg = JSON.parse(
4243
+ await readFile9(join20(rnessDir, "package.json"), "utf8")
4244
+ );
4245
+ if (typeof pkg.packageManager === "string") return pkg.packageManager;
4246
+ } catch {
4247
+ }
4248
+ return `${pm}@${await packageManagerVersion(pm)}`;
4249
+ }
4250
+ function finish(ui, cwd, rnessDir, label, target) {
4251
+ const rel = relative5(cwd, rnessDir) || ".";
4252
+ const next = [
4253
+ `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
4254
+ "# teammates: git pull \u2014 the next rness command installs it"
4255
+ ];
4256
+ if (ui.session) {
4257
+ ui.note("Next steps", next);
4258
+ ui.outro(`${label} runs @rness/cli ${target}`);
4259
+ } else
4260
+ process.stdout.write(
4261
+ [
4262
+ "",
4263
+ status("upgraded", `${label} to @rness/cli ${target}`),
4264
+ "",
4265
+ "Next:",
4266
+ ...next.map((line) => ` ${line}`),
4267
+ ""
4268
+ ].join("\n")
4269
+ );
4270
+ return 0;
4271
+ }
4272
+ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps = {}) {
3708
4273
  const cwd = opts.cwd ?? process.cwd();
3709
4274
  if (version !== void 0 && !EXACT_VERSION.test(version)) {
3710
4275
  process.stderr.write(
@@ -3746,7 +4311,21 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3746
4311
  return 1;
3747
4312
  }
3748
4313
  const installed = (await readPinnedCli(ws.rnessDir))?.version ?? null;
3749
- if (pin.spec === target && installed === target) {
4314
+ if (!await isRepository2(ws.rnessDir)) {
4315
+ process.stderr.write(`${label} is not a git repository
4316
+ `);
4317
+ return 1;
4318
+ }
4319
+ if (!await isClean(ws.rnessDir)) {
4320
+ process.stderr.write(
4321
+ `${label} has uncommitted changes; commit or stash them, then ${rnessCommand()} upgrade
4322
+ `
4323
+ );
4324
+ return 1;
4325
+ }
4326
+ const base = await findScaffoldBase(ws.rnessDir);
4327
+ const scaffoldCurrent = base !== null && base.version === target;
4328
+ if (pin.spec === target && installed === target && scaffoldCurrent) {
3750
4329
  if (ui.session) ui.outro(`Already at ${target}`);
3751
4330
  else process.stdout.write(`already at ${target}
3752
4331
  `);
@@ -3761,7 +4340,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3761
4340
  return 1;
3762
4341
  }
3763
4342
  const pm = await workspacePackageManager(ws.rnessDir);
3764
- if (pin.spec === target)
4343
+ if (pin.spec === target && installed === target)
4344
+ ui.line("scaffold", `@rness/cli ${target} \u2014 merging it into ${label}`);
4345
+ else if (pin.spec === target)
3765
4346
  ui.line(
3766
4347
  "install",
3767
4348
  `@rness/cli ${target} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
@@ -3783,46 +4364,58 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3783
4364
  return 1;
3784
4365
  }
3785
4366
  }
3786
- const packageFile = join18(ws.rnessDir, "package.json");
3787
- const restore = /* @__PURE__ */ new Map([
3788
- [packageFile, await readFile9(packageFile, "utf8")]
3789
- ]);
3790
- if (pin.spec !== target) await writePin(ws.rnessDir, pin.spec, target);
3791
- const workflows = join18(ws.rnessDir, ".github", "workflows");
3792
- for (const name of await readdir3(workflows).catch(() => [])) {
3793
- if (!/\.ya?ml$/.test(name)) continue;
3794
- const file = join18(workflows, name);
3795
- const text = await readOrNull(file);
3796
- if (text === null) continue;
3797
- const shown = `.github/workflows/${name}`;
3798
- if (name === "validate.yml" && OLD_WORKFLOW_RUN.test(text)) {
3799
- restore.set(file, text);
3800
- await writeFileAtomic(
3801
- file,
3802
- text.replace(OLD_WORKFLOW_RUN, () => WORKFLOW_RUN)
4367
+ let merged = false;
4368
+ if (!scaffoldCurrent) {
4369
+ const source = await (deps.scaffoldFor ?? scaffoldFor)(
4370
+ target,
4371
+ ws.rnessDir
4372
+ );
4373
+ try {
4374
+ const files = await renderScaffold(source.dir, {
4375
+ version: target,
4376
+ packageManager: await declaredManager(ws.rnessDir, pm)
4377
+ });
4378
+ if (base === null)
4379
+ ui.line(
4380
+ "adopting",
4381
+ `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`
4382
+ );
4383
+ else ui.line("merging", `the @rness/cli ${target} scaffold`);
4384
+ const commit = await buildScaffoldCommit(
4385
+ ws.rnessDir,
4386
+ base?.commit ?? null,
4387
+ files,
4388
+ target
3803
4389
  );
3804
- ui.line(
3805
- "updated",
3806
- `${shown} (it now reads the version from package.json)`
4390
+ const result = await mergeScaffold(
4391
+ ws.rnessDir,
4392
+ commit,
4393
+ base?.commit ?? null
3807
4394
  );
3808
- } else if (pin.spec !== target && text.includes(`@rness/cli@${pin.spec}`)) {
3809
- ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
4395
+ for (const [change, path] of result.changes) ui.line(change, path);
4396
+ if (result.conflicts.length > 0) {
4397
+ for (const path of result.conflicts) ui.error(`conflict ${path}`);
4398
+ const rel = relative5(cwd, ws.rnessDir) || ".";
4399
+ ui.hint(
4400
+ `resolve them in ${rel} (git checkout --ours <file> keeps yours, --theirs takes the scaffold's, or edit), git add, git commit, then ${rnessCommand()} upgrade again`,
4401
+ "stderr"
4402
+ );
4403
+ return 1;
4404
+ }
4405
+ merged = true;
4406
+ } finally {
4407
+ await source.cleanup?.();
3810
4408
  }
3811
4409
  }
3812
- const dependabot = join18(ws.rnessDir, ".github", "dependabot.yml");
3813
- if (await exists(dependabot)) {
3814
- ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
3815
- } else {
3816
- await mkdir6(dirname7(dependabot), { recursive: true });
3817
- await writeFileAtomic(
3818
- dependabot,
3819
- await readFile9(join18(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
3820
- );
3821
- ui.line(
3822
- "wrote",
3823
- ".rness/.github/dependabot.yml (@rness/cli now moves by pull request)"
3824
- );
3825
- }
4410
+ const packageFile = join20(ws.rnessDir, "package.json");
4411
+ const restore = /* @__PURE__ */ new Map([
4412
+ [packageFile, await readFile9(packageFile, "utf8")]
4413
+ ]);
4414
+ const moved = await readPin(ws.rnessDir);
4415
+ if (moved !== null && moved.spec !== target)
4416
+ await writePin(ws.rnessDir, moved.spec, target);
4417
+ if (pin.spec === target && installed === target)
4418
+ return finish(ui, cwd, ws.rnessDir, label, target);
3826
4419
  try {
3827
4420
  await ui.step(
3828
4421
  { doing: `installing dependencies with ${pm}` },
@@ -3830,10 +4423,20 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3830
4423
  () => ["installed", `dependencies with ${pm}`]
3831
4424
  );
3832
4425
  } catch (e) {
3833
- for (const [file, text] of restore) await writeFileAtomic(file, text);
3834
4426
  const message = e instanceof Error ? e.message : String(e);
3835
4427
  ui.error(message);
3836
- ui.hint(`restored ${relative5(cwd, packageFile) || packageFile}`, "stderr");
4428
+ if (!merged) {
4429
+ for (const [file, text] of restore) await writeFileAtomic(file, text);
4430
+ ui.hint(
4431
+ `restored ${relative5(cwd, packageFile) || packageFile}`,
4432
+ "stderr"
4433
+ );
4434
+ }
4435
+ if (merged)
4436
+ ui.hint(
4437
+ `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4438
+ "stderr"
4439
+ );
3837
4440
  if (/minimum.?release.?age|NO_MATURE/i.test(message))
3838
4441
  ui.hint(
3839
4442
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
@@ -3852,33 +4455,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3852
4455
  ui.gitIsSilent = true;
3853
4456
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
3854
4457
  if (code !== 0) return code;
3855
- const rel = relative5(cwd, ws.rnessDir) || ".";
3856
- const next = [
3857
- `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
3858
- "# teammates: git pull \u2014 the next rness command installs it"
3859
- ];
3860
- if (ui.session) {
3861
- ui.note("Next steps", next);
3862
- ui.outro(`${label} runs @rness/cli ${target}`);
3863
- } else
3864
- process.stdout.write(
3865
- [
3866
- "",
3867
- status("upgraded", `${label} to @rness/cli ${target}`),
3868
- "",
3869
- "Next:",
3870
- ...next.map((line) => ` ${line}`),
3871
- ""
3872
- ].join("\n")
3873
- );
3874
- return 0;
4458
+ return finish(ui, cwd, ws.rnessDir, label, target);
3875
4459
  } catch (e) {
3876
4460
  return reportError(e);
3877
4461
  }
3878
4462
  }
3879
4463
 
3880
4464
  // src/core/blocks.ts
3881
- import { join as join19 } from "path";
4465
+ import { join as join21 } from "path";
3882
4466
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
3883
4467
  async function checkBlocks({
3884
4468
  root,
@@ -3895,7 +4479,7 @@ async function checkBlocks({
3895
4479
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
3896
4480
  checks
3897
4481
  });
3898
- if (!await exists(join19(root, "org"))) return derive();
4482
+ if (!await exists(join21(root, "org"))) return derive();
3899
4483
  const targets = [
3900
4484
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
3901
4485
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -3904,7 +4488,7 @@ async function checkBlocks({
3904
4488
  const segments = entry.path.split("/");
3905
4489
  targets.push({
3906
4490
  scope: name,
3907
- dir: join19(root, ...segments),
4491
+ dir: join21(root, ...segments),
3908
4492
  depth: segments.length,
3909
4493
  file: "AGENTS.md",
3910
4494
  label: `${entry.path}/AGENTS.md`
@@ -3919,7 +4503,7 @@ async function checkBlocks({
3919
4503
  );
3920
4504
  continue;
3921
4505
  }
3922
- const text = await readOrNull(join19(t.dir, t.file));
4506
+ const text = await readOrNull(join21(t.dir, t.file));
3923
4507
  const lines = text === null ? [] : text.split(/\r?\n/);
3924
4508
  const found = findBlock(lines);
3925
4509
  if (found.kind === "none") {
@@ -3959,7 +4543,7 @@ async function checkBlocks({
3959
4543
  }
3960
4544
 
3961
4545
  // src/core/contract.ts
3962
- import { join as join20 } from "path";
4546
+ import { join as join22 } from "path";
3963
4547
  var STATUSES = {
3964
4548
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
3965
4549
  specs: [
@@ -3978,7 +4562,7 @@ async function checkContract(rnessDir) {
3978
4562
  const problems = [];
3979
4563
  for (const name of COLLECTIONS) {
3980
4564
  const allowed = STATUSES[name];
3981
- const items = await collectMarkdown(join20(rnessDir, name));
4565
+ const items = await collectMarkdown(join22(rnessDir, name));
3982
4566
  for (const item of items) {
3983
4567
  const where = `${name}/${item.rel}`;
3984
4568
  if (SKIP.has(where)) continue;
@@ -4030,11 +4614,35 @@ async function validateCommand(opts, deps = {}) {
4030
4614
  org
4031
4615
  });
4032
4616
  problems.push(...blocks.problems);
4617
+ for (const name of unsupportedAgents(manifest.agents ?? []))
4618
+ problems.push(unsupportedMessage(name));
4619
+ const targets = await agentTargets(ws.root, manifest, { check: true });
4620
+ for (const o of targets)
4621
+ if (o.status === "stale")
4622
+ problems.push(
4623
+ `${o.label}: ${o.detail ?? ""} (run ${rnessCommand()} sync)`
4624
+ );
4625
+ else if (o.status === "invalid")
4626
+ problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4627
+ const scaffoldWarnings = [];
4628
+ if (await isRepository2(ws.rnessDir) && !await isShallow(ws.rnessDir)) {
4629
+ const pin = await readPin(ws.rnessDir);
4630
+ const base = await findScaffoldBase(ws.rnessDir);
4631
+ if (base === null || base.version === null)
4632
+ scaffoldWarnings.push(
4633
+ `the scaffold is not tracked in .rness yet: run ${rnessCommand()} upgrade`
4634
+ );
4635
+ else if (pin !== null && EXACT_VERSION.test(pin.spec) && pin.spec !== base.version)
4636
+ scaffoldWarnings.push(
4637
+ `the @rness/cli ${pin.spec} scaffold is not merged here: run ${rnessCommand()} upgrade`
4638
+ );
4639
+ }
4033
4640
  const ui = await makeUi(deps.terminal ?? defaultTerminal);
4034
4641
  if (!ui.session) {
4035
4642
  for (const p of problems) process.stderr.write(`${p}
4036
4643
  `);
4037
- for (const w of blocks.warnings) process.stderr.write(`${warn(w)}
4644
+ for (const w of [...blocks.warnings, ...scaffoldWarnings])
4645
+ process.stderr.write(`${warn(w)}
4038
4646
  `);
4039
4647
  if (problems.length > 0) return 1;
4040
4648
  process.stdout.write("context ok\n");
@@ -4052,7 +4660,10 @@ async function validateCommand(opts, deps = {}) {
4052
4660
  else if (c.status === "stale" || c.status === "malformed")
4053
4661
  ui.error(c.message);
4054
4662
  else ui.warn(c.message);
4663
+ for (const o of targets)
4664
+ if (o.status === "unchanged") ui.line("current", o.label);
4055
4665
  for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
4666
+ for (const w of scaffoldWarnings) ui.warn(w);
4056
4667
  ui.outro(
4057
4668
  problems.length > 0 ? "Context mismatch" : "Context OK",
4058
4669
  problems.length > 0 ? "error" : "done"
@@ -4126,7 +4737,11 @@ function buildProgram(state) {
4126
4737
  ).option("--scope <name>", "only this scope (the root block is skipped)").option(
4127
4738
  "--check",
4128
4739
  "render and compare only; exit 1 when a block is out of date"
4129
- ).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) => {
4740
+ ).option("--pull", "git pull --ff-only in every clean clone").option("--all", "clone every rness.json repository missing from org/").option(
4741
+ "--agent <name>",
4742
+ "declare an agent in rness.json, then write its files (repeatable; supported: claude)",
4743
+ (value, previous = []) => [...previous, value]
4744
+ ).option("-y, --yes", "do not ask for confirmation").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
4130
4745
  state.code = await syncCommand(opts);
4131
4746
  });
4132
4747
  program.command("add").description(
@@ -4174,6 +4789,10 @@ function buildProgram(state) {
4174
4789
  ).option("--https", "force https://github.com/").option(
4175
4790
  "--blank",
4176
4791
  "a local workspace with no GitHub organization: .rness/, an empty org/, the root files"
4792
+ ).option(
4793
+ "--agent <name>",
4794
+ "a new workspace's agent, declared in rness.json (repeatable; supported: claude)",
4795
+ (value, previous = []) => [...previous, value]
4177
4796
  ).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) => {
4178
4797
  if (opts.blank === true) {
4179
4798
  state.code = await createCommand(
@@ -4196,7 +4815,7 @@ function buildProgram(state) {
4196
4815
  });
4197
4816
  return program;
4198
4817
  }
4199
- async function run(argv) {
4818
+ async function run2(argv) {
4200
4819
  const state = { code: 0 };
4201
4820
  const program = buildProgram(state);
4202
4821
  if (argv.length === 0) {
@@ -4289,6 +4908,7 @@ export {
4289
4908
  defaultTransport,
4290
4909
  findWorkspace,
4291
4910
  reportError,
4911
+ VERSION,
4292
4912
  BEGIN,
4293
4913
  END,
4294
4914
  BLOCK_SIZE_WARNING,
@@ -4310,7 +4930,6 @@ export {
4310
4930
  addCommand,
4311
4931
  renderMarkdown,
4312
4932
  contextCommand,
4313
- VERSION,
4314
4933
  findDelegate,
4315
4934
  SCAFFOLD_FILES,
4316
4935
  scaffoldDir,
@@ -4323,8 +4942,8 @@ export {
4323
4942
  STATUSES,
4324
4943
  checkContract,
4325
4944
  validateCommand,
4326
- run,
4945
+ run2 as run,
4327
4946
  OWN_COMMANDS,
4328
4947
  catchUp
4329
4948
  };
4330
- //# sourceMappingURL=chunk-7TOHFPW6.js.map
4949
+ //# sourceMappingURL=chunk-VQMX52HT.js.map