@wairon/cli 5.1.1-dev.101 → 5.1.1-dev.102

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.101";
68
+ WAIRON_VERSION = "5.1.1-dev.102";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -214,6 +214,7 @@ __export(fs_exports, {
214
214
  getRequestProjectRoot: () => getRequestProjectRoot,
215
215
  getWriteReach: () => getWriteReach,
216
216
  isOutsideRoot: () => isOutsideRoot,
217
+ lineEndingFor: () => lineEndingFor,
217
218
  listFiles: () => listFiles,
218
219
  listFilesRecursive: () => listFilesRecursive,
219
220
  pathExists: () => pathExists,
@@ -222,6 +223,7 @@ __export(fs_exports, {
222
223
  runWithProjectBinding: () => runWithProjectBinding,
223
224
  runWithProjectRoot: () => runWithProjectRoot,
224
225
  setProjectRoot: () => setProjectRoot,
226
+ withLineEndings: () => withLineEndings,
225
227
  writeFile: () => writeFile,
226
228
  writeFileIfChanged: () => writeFileIfChanged,
227
229
  writeReachDenials: () => writeReachDenials,
@@ -232,17 +234,118 @@ function ensureDir(dirPath) {
232
234
  }
233
235
  function writeFile(filePath, content) {
234
236
  ensureDir(path2.dirname(filePath));
235
- fs.writeFileSync(filePath, content, "utf-8");
237
+ fs.writeFileSync(filePath, withLineEndings(filePath, content), "utf-8");
236
238
  }
237
239
  function writeFileIfChanged(filePath, content) {
240
+ const text3 = withLineEndings(filePath, content);
238
241
  if (fs.existsSync(filePath)) {
239
242
  const existing = fs.readFileSync(filePath, "utf-8");
240
- if (existing === content) return false;
243
+ if (existing === text3) return false;
241
244
  }
242
245
  ensureDir(path2.dirname(filePath));
243
- fs.writeFileSync(filePath, content, "utf-8");
246
+ fs.writeFileSync(filePath, text3, "utf-8");
244
247
  return true;
245
248
  }
249
+ function endingsOf(text3) {
250
+ const crlf = text3.split("\r\n").length - 1;
251
+ const lf = text3.split("\n").length - 1 - crlf;
252
+ if (crlf === 0 && lf === 0) return null;
253
+ return crlf > lf ? "\r\n" : "\n";
254
+ }
255
+ function sampleOf(file) {
256
+ let fd;
257
+ try {
258
+ fd = fs.openSync(file, "r");
259
+ const buffer = Buffer.alloc(EOL_SAMPLE_BYTES);
260
+ const read2 = fs.readSync(fd, buffer, 0, EOL_SAMPLE_BYTES, 0);
261
+ return buffer.subarray(0, read2).toString("utf-8");
262
+ } catch {
263
+ return null;
264
+ } finally {
265
+ if (fd !== void 0) fs.closeSync(fd);
266
+ }
267
+ }
268
+ function attributeMatches(pattern, relative28) {
269
+ const glob = pattern.replace(/^\//, "");
270
+ const target = glob.includes("/") ? relative28 : path2.posix.basename(relative28);
271
+ const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
272
+ return new RegExp(`^${source}$`).test(target);
273
+ }
274
+ function attributeEol(attributesFile, file) {
275
+ const text3 = readFileOrNull(attributesFile);
276
+ if (text3 === null) return null;
277
+ const relative28 = path2.relative(path2.dirname(attributesFile), file).split(path2.sep).join("/");
278
+ let eol = null;
279
+ for (const raw of text3.split(/\r?\n/)) {
280
+ const line2 = raw.trim();
281
+ if (line2 === "" || line2.startsWith("#")) continue;
282
+ const [pattern, ...attrs] = line2.split(/\s+/);
283
+ if (!attributeMatches(pattern, relative28)) continue;
284
+ for (const attr of attrs) {
285
+ if (attr === "eol=crlf") eol = "\r\n";
286
+ else if (attr === "eol=lf") eol = "\n";
287
+ else if (attr === "-text" || attr === "binary") eol = null;
288
+ }
289
+ }
290
+ return eol;
291
+ }
292
+ function conventionFolders(dir) {
293
+ const folders = [];
294
+ let at = path2.resolve(dir);
295
+ for (let depth = 0; depth < 8; depth++) {
296
+ folders.push(at);
297
+ if (fs.existsSync(path2.join(at, ".git")) || fs.existsSync(path2.join(at, ".wai"))) return folders;
298
+ const parent = path2.dirname(at);
299
+ if (parent === at) break;
300
+ at = parent;
301
+ }
302
+ return folders.slice(0, 1);
303
+ }
304
+ function folderEndings(folder, ext, exclude) {
305
+ let names;
306
+ try {
307
+ names = fs.readdirSync(folder);
308
+ } catch {
309
+ return null;
310
+ }
311
+ const candidates = names.filter((n) => path2.join(folder, n) !== exclude && /\.(ya?ml|json|md|ts|js|txt)$/i.test(n)).sort((a, b) => Number(path2.extname(b) === ext) - Number(path2.extname(a) === ext)).slice(0, 24);
312
+ let crlf = 0;
313
+ let lf = 0;
314
+ for (const name of candidates) {
315
+ const full = path2.join(folder, name);
316
+ try {
317
+ if (!fs.statSync(full).isFile()) continue;
318
+ } catch {
319
+ continue;
320
+ }
321
+ const sample = sampleOf(full);
322
+ const endings = sample === null ? null : endingsOf(sample);
323
+ if (endings === "\r\n") crlf++;
324
+ else if (endings === "\n") lf++;
325
+ }
326
+ if (crlf === lf) return null;
327
+ return crlf > lf ? "\r\n" : "\n";
328
+ }
329
+ function lineEndingFor(filePath) {
330
+ const own = sampleOf(filePath);
331
+ const existing = own === null ? null : endingsOf(own);
332
+ if (existing) return existing;
333
+ const folders = conventionFolders(path2.dirname(filePath));
334
+ for (const folder of folders) {
335
+ const eol = attributeEol(path2.join(folder, ".gitattributes"), filePath);
336
+ if (eol) return eol;
337
+ }
338
+ const ext = path2.extname(filePath);
339
+ for (const folder of folders) {
340
+ const endings = folderEndings(folder, ext, path2.resolve(filePath));
341
+ if (endings) return endings;
342
+ }
343
+ return "\n";
344
+ }
345
+ function withLineEndings(filePath, content) {
346
+ const normalised = content.replace(/\r\n/g, "\n");
347
+ return lineEndingFor(filePath) === "\r\n" ? normalised.replace(/\n/g, "\r\n") : normalised;
348
+ }
246
349
  function readFileOrNull(filePath) {
247
350
  try {
248
351
  return fs.readFileSync(filePath, "utf-8");
@@ -384,19 +487,154 @@ function aiDirAt(root, ...segments) {
384
487
  const base = !fs.existsSync(waiPath) && fs.existsSync(waironPath) ? waironPath : waiPath;
385
488
  return path2.join(base, ...segments);
386
489
  }
387
- var fs, path2, import_node_async_hooks, projectRootOverride, sameRoot, requestRootStore;
490
+ var fs, path2, import_node_async_hooks, EOL_SAMPLE_BYTES, projectRootOverride, sameRoot, requestRootStore;
388
491
  var init_fs = __esm({
389
492
  "src/utils/fs.ts"() {
390
493
  "use strict";
391
494
  fs = __toESM(require("fs"));
392
495
  path2 = __toESM(require("path"));
393
496
  import_node_async_hooks = require("async_hooks");
497
+ EOL_SAMPLE_BYTES = 64 * 1024;
394
498
  projectRootOverride = null;
395
499
  sameRoot = (a, b) => path2.resolve(a) === path2.resolve(b);
396
500
  requestRootStore = new import_node_async_hooks.AsyncLocalStorage();
397
501
  }
398
502
  });
399
503
 
504
+ // src/core/adapters/git-source.ts
505
+ function osUserCache() {
506
+ if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path3.join(os2.homedir(), "AppData", "Local");
507
+ if (process.platform === "darwin") return path3.join(os2.homedir(), "Library", "Caches");
508
+ return process.env.XDG_CACHE_HOME ?? path3.join(os2.homedir(), ".cache");
509
+ }
510
+ function cacheRoot() {
511
+ const setting = process.env.WAIRON_CACHE_DIR ?? "";
512
+ let root = cacheRoots.get(setting);
513
+ if (root === void 0) {
514
+ root = setting !== "" ? path3.resolve(setting) : path3.join(osUserCache(), "wairon");
515
+ cacheRoots.set(setting, root);
516
+ }
517
+ return root;
518
+ }
519
+ function git(args, cwd) {
520
+ return (0, import_child_process.execFileSync)("git", args, {
521
+ cwd,
522
+ encoding: "utf8",
523
+ stdio: ["ignore", "pipe", "pipe"],
524
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
525
+ });
526
+ }
527
+ function reasonOf(e) {
528
+ const stderr = e?.stderr;
529
+ const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
530
+ return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
531
+ }
532
+ function cacheEntryOf(url, commit3) {
533
+ const repo = crypto.createHash("sha256").update(url).digest("hex");
534
+ return path3.join(cacheRoot(), "git", repo, commit3);
535
+ }
536
+ function within(entry, url, commit3, dir) {
537
+ const at = dir ? path3.join(entry, dir) : entry;
538
+ if (!fs2.existsSync(at)) {
539
+ throw new SourceUnavailable(`the commit ${commit3} of ${url} holds no "${dir}"`);
540
+ }
541
+ return at;
542
+ }
543
+ function fetchInto(tmp, url, commit3) {
544
+ git(["init", "-q"], tmp);
545
+ try {
546
+ git(["fetch", "-q", "--depth", "1", url, commit3], tmp);
547
+ } catch {
548
+ git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
549
+ }
550
+ git(["-c", "advice.detachedHead=false", "checkout", "-q", commit3], tmp);
551
+ fs2.rmSync(path3.join(tmp, ".git"), { recursive: true, force: true });
552
+ }
553
+ function fetch2(url, commit3, dir) {
554
+ const entry = cacheEntryOf(url, commit3);
555
+ if (fs2.existsSync(entry)) return within(entry, url, commit3, dir);
556
+ fs2.mkdirSync(path3.dirname(entry), { recursive: true });
557
+ const tmp = `${entry}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
558
+ fs2.mkdirSync(tmp, { recursive: true });
559
+ try {
560
+ fetchInto(tmp, url, commit3);
561
+ try {
562
+ fs2.renameSync(tmp, entry);
563
+ } catch (e) {
564
+ if (!fs2.existsSync(entry)) throw e;
565
+ fs2.rmSync(tmp, { recursive: true, force: true });
566
+ }
567
+ } catch (e) {
568
+ fs2.rmSync(tmp, { recursive: true, force: true });
569
+ if (fs2.readdirSync(path3.dirname(entry)).length === 0) fs2.rmdirSync(path3.dirname(entry));
570
+ throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit3} of ${url}: ${reasonOf(e)}`);
571
+ }
572
+ return within(entry, url, commit3, dir);
573
+ }
574
+ function resolve3(url, ref) {
575
+ let listed;
576
+ try {
577
+ listed = git(["ls-remote", url, ref ?? "HEAD"]);
578
+ } catch (e) {
579
+ throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
580
+ }
581
+ const wanted = commitListed(listed, ref ?? "HEAD");
582
+ if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
583
+ return wanted;
584
+ }
585
+ function commitListed(listed, ref) {
586
+ const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
587
+ const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
588
+ for (const name of names) {
589
+ const line2 = lines.find(([, listedName]) => listedName === name);
590
+ if (line2) return line2[0];
591
+ }
592
+ return void 0;
593
+ }
594
+ function trackedFiles(directory, pathspecs) {
595
+ if (pathspecs.length === 0) return [];
596
+ try {
597
+ return git(["ls-files", "-z", "--", ...pathspecs], directory).split("\0").filter((p) => p !== "");
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+ function head(directory) {
603
+ try {
604
+ return git(["rev-parse", "HEAD"], directory).trim() || null;
605
+ } catch {
606
+ return null;
607
+ }
608
+ }
609
+ function repositoryRoot(directory) {
610
+ let at = path3.resolve(directory);
611
+ for (; ; ) {
612
+ if (fs2.existsSync(path3.join(at, ".git"))) return at;
613
+ const up = path3.dirname(at);
614
+ if (up === at) return null;
615
+ at = up;
616
+ }
617
+ }
618
+ var import_child_process, crypto, fs2, os2, path3, SourceUnavailable, cacheRoots;
619
+ var init_git_source = __esm({
620
+ "src/core/adapters/git-source.ts"() {
621
+ "use strict";
622
+ import_child_process = require("child_process");
623
+ crypto = __toESM(require("crypto"));
624
+ fs2 = __toESM(require("fs"));
625
+ os2 = __toESM(require("os"));
626
+ path3 = __toESM(require("path"));
627
+ init_errors();
628
+ SourceUnavailable = class extends WaironError {
629
+ constructor(message) {
630
+ super(message);
631
+ this.name = "SourceUnavailable";
632
+ }
633
+ };
634
+ cacheRoots = /* @__PURE__ */ new Map();
635
+ }
636
+ });
637
+
400
638
  // src/utils/yaml.ts
401
639
  function parseYaml(content, sourcePath) {
402
640
  try {
@@ -720,7 +958,7 @@ function activeTargetTypes(config) {
720
958
  function declaredPackNames(config) {
721
959
  const selection = config.profileSelection;
722
960
  return [.../* @__PURE__ */ new Set([
723
- ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path3.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
961
+ ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path4.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
724
962
  ...selection?.requiredPackNames ?? [],
725
963
  ...selection?.defaultPackNames ?? []
726
964
  ])];
@@ -999,11 +1237,11 @@ function declaredMembers(config) {
999
1237
  function isPart(config) {
1000
1238
  return config?.partOf !== void 0;
1001
1239
  }
1002
- var path3, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ExternalSourceSchema, ExternalDeclarationSchema, MemberDeclarationSchema, PartOfSchema, plainString, PackRequirementSchema, CompositionConfigSchema, ProjectConfigSchema, PACK_EXT_RE, CONCRETE_RE, PARTIAL_RE, OPERATOR_RE, text, cv, PROJECT_ID_RE, EXTERNAL_ALIAS_RE, USE_ENTRY_RE, FULL_COMMIT_RE, GIT_URL_RE;
1240
+ var path4, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ExternalSourceSchema, ExternalDeclarationSchema, MemberDeclarationSchema, PartOfSchema, plainString, PackRequirementSchema, CompositionConfigSchema, ProjectConfigSchema, PACK_EXT_RE, CONCRETE_RE, PARTIAL_RE, OPERATOR_RE, text, cv, PROJECT_ID_RE, EXTERNAL_ALIAS_RE, USE_ENTRY_RE, FULL_COMMIT_RE, GIT_URL_RE;
1003
1241
  var init_project = __esm({
1004
1242
  "src/models/project.ts"() {
1005
1243
  "use strict";
1006
- path3 = __toESM(require("path"));
1244
+ path4 = __toESM(require("path"));
1007
1245
  import_zod3 = require("zod");
1008
1246
  init_agent();
1009
1247
  init_execution();
@@ -1335,7 +1573,7 @@ var init_project = __esm({
1335
1573
  /**
1336
1574
  * The producer's public names this project imports, so its specs may name
1337
1575
  * them bare: `['*']` imports every public name the producer exports to this
1338
- * project, `[waffler-error]` only those. Plain strings: a malformed entry is
1576
+ * project, `[shared-error]` only those. Plain strings: a malformed entry is
1339
1577
  * REPORTED (the declaration's problem), never unreadable.
1340
1578
  */
1341
1579
  use: import_zod3.z.array(import_zod3.z.string()).optional(),
@@ -1546,13 +1784,13 @@ var init_project = __esm({
1546
1784
 
1547
1785
  // src/config/project-config.ts
1548
1786
  function waiDirAt(root) {
1549
- const wai = path4.join(root, ".wai");
1550
- const legacy = path4.join(root, ".wairon");
1551
- return !fs2.existsSync(wai) && fs2.existsSync(legacy) ? legacy : wai;
1787
+ const wai = path5.join(root, ".wai");
1788
+ const legacy = path5.join(root, ".wairon");
1789
+ return !fs3.existsSync(wai) && fs3.existsSync(legacy) ? legacy : wai;
1552
1790
  }
1553
1791
  function projectConfigFsAdapterAt(rootDir) {
1554
- const root = path4.resolve(rootDir);
1555
- const file = () => path4.join(waiDirAt(root), "project.yaml");
1792
+ const root = path5.resolve(rootDir);
1793
+ const file = () => path5.join(waiDirAt(root), "project.yaml");
1556
1794
  return {
1557
1795
  readDocument() {
1558
1796
  return readYamlFile(file());
@@ -1566,7 +1804,7 @@ function projectConfigFsAdapterAt(rootDir) {
1566
1804
  writeFile(file(), editDocumentText(text3, document, root));
1567
1805
  },
1568
1806
  documentExists() {
1569
- return fs2.existsSync(file());
1807
+ return fs3.existsSync(file());
1570
1808
  },
1571
1809
  readText() {
1572
1810
  return readFileOrNull(file());
@@ -1588,8 +1826,9 @@ function storeOver(adapter, root) {
1588
1826
  if (!checked.success) {
1589
1827
  throw new WaironError(`Refusing to write an invalid .wai/project.yaml at ${root}: ${checked.error.message}`);
1590
1828
  }
1591
- const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : config;
1592
- adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, currentDocument()));
1829
+ const onDisk = currentDocument();
1830
+ const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : withoutDefaultedMaterialization(config, onDisk);
1831
+ adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, onDisk));
1593
1832
  },
1594
1833
  exists() {
1595
1834
  return adapter.documentExists();
@@ -1846,9 +2085,67 @@ function asReadable(document) {
1846
2085
  if (!isPlainObject(document) || document.partOf === void 0) return document;
1847
2086
  return { name: "", createdAt: PART_EPOCH, updatedAt: PART_EPOCH, ...document };
1848
2087
  }
2088
+ function holdsMaterializedAgentFiles(document, root) {
2089
+ const dirs = new Set(Object.values(DEFAULT_TARGET_DIRS));
2090
+ for (const target of Array.isArray(document.targets) ? document.targets : []) {
2091
+ const outputDir = isPlainObject(target) ? target.outputDir : void 0;
2092
+ if (typeof outputDir === "string") dirs.add(outputDir);
2093
+ }
2094
+ const base = path5.resolve(root);
2095
+ const inside = [];
2096
+ for (const dir of dirs) {
2097
+ const rel2 = path5.relative(base, path5.resolve(base, dir));
2098
+ if (rel2.startsWith("..") || path5.isAbsolute(rel2)) continue;
2099
+ let present = false;
2100
+ try {
2101
+ present = fs3.statSync(path5.join(base, rel2)).isDirectory();
2102
+ } catch {
2103
+ present = false;
2104
+ }
2105
+ if (present) inside.push(rel2 === "" ? "." : rel2.split(path5.sep).join("/"));
2106
+ }
2107
+ if (inside.length === 0) return false;
2108
+ const tracked = trackedFiles(base, inside);
2109
+ const candidates = [];
2110
+ if (tracked === null) {
2111
+ for (const dir of inside) {
2112
+ let names;
2113
+ try {
2114
+ names = fs3.readdirSync(path5.join(base, dir));
2115
+ } catch {
2116
+ continue;
2117
+ }
2118
+ for (const name of names) candidates.push(path5.posix.join(dir, name));
2119
+ }
2120
+ } else {
2121
+ const folders = new Set(inside.map((dir) => path5.posix.normalize(dir)));
2122
+ for (const file of tracked) if (folders.has(path5.posix.dirname(file))) candidates.push(file);
2123
+ }
2124
+ for (const file of candidates) {
2125
+ const head2 = readFileOrNull(path5.join(base, file));
2126
+ if (head2 === null) continue;
2127
+ if (MANAGED_AGENT_NAME.test(path5.posix.basename(file))) return true;
2128
+ if (head2.slice(0, 4096).includes(MANAGED_AGENT_MARKER)) return true;
2129
+ }
2130
+ return false;
2131
+ }
2132
+ function withInferredMaterialization(document, root) {
2133
+ if (!isPlainObject(document) || document.partOf !== void 0) return document;
2134
+ const rules = isPlainObject(document.rules) ? document.rules : void 0;
2135
+ if (rules !== void 0 && hasOwn(rules, "materializeAgentFiles")) return document;
2136
+ if (!holdsMaterializedAgentFiles(document, root)) return document;
2137
+ return { ...document, rules: { ...rules ?? {}, materializeAgentFiles: true } };
2138
+ }
2139
+ function withoutDefaultedMaterialization(config, onDisk) {
2140
+ if (!isPlainObject(onDisk) || config.rules?.materializeAgentFiles !== false) return config;
2141
+ const stated = isPlainObject(onDisk.rules) && hasOwn(onDisk.rules, "materializeAgentFiles");
2142
+ if (stated) return config;
2143
+ const { materializeAgentFiles: _defaulted, ...rules } = config.rules;
2144
+ return { ...config, rules };
2145
+ }
1849
2146
  function parseConfig(document, root) {
1850
2147
  try {
1851
- return ProjectConfigSchema.parse(asReadable(document));
2148
+ return ProjectConfigSchema.parse(asReadable(withInferredMaterialization(document, root)));
1852
2149
  } catch (e) {
1853
2150
  throw new WaironError(
1854
2151
  `Invalid .wai/project.yaml: ${e instanceof Error ? e.message : String(e)}
@@ -2287,15 +2584,15 @@ function indexOver(store, root) {
2287
2584
  specsDir() {
2288
2585
  try {
2289
2586
  const declared = store.readField("paths.specsDir");
2290
- if (declared) return path4.resolve(root, declared);
2587
+ if (declared) return path5.resolve(root, declared);
2291
2588
  } catch {
2292
2589
  }
2293
- return path4.join(waiDirAt(root), "specs");
2590
+ return path5.join(waiDirAt(root), "specs");
2294
2591
  }
2295
2592
  };
2296
2593
  }
2297
2594
  function projectConfigRepositoryOver(adapter, rootDir) {
2298
- const root = path4.resolve(rootDir);
2595
+ const root = path5.resolve(rootDir);
2299
2596
  const store = storeOver(adapter, root);
2300
2597
  const index = indexOver(store, root);
2301
2598
  const registry = registryOver(store, root);
@@ -2389,15 +2686,17 @@ function projectConfigRepositoryOver(adapter, rootDir) {
2389
2686
  function projectConfigRepositoryAt(rootDir) {
2390
2687
  return projectConfigRepositoryOver(projectConfigFsAdapterAt(rootDir), rootDir);
2391
2688
  }
2392
- var fs2, path4, YAML, import_zod4, KEY_RE, FRESH_TEXT_OPTIONS, PARSE_OPTIONS, DocumentReconciler, PART_EPOCH, USE_GLOBAL_PACKS_DEFAULT, PROJECT_FIELDS, bound, projectConfigRepository;
2689
+ var fs3, path5, YAML, import_zod4, KEY_RE, FRESH_TEXT_OPTIONS, PARSE_OPTIONS, DocumentReconciler, PART_EPOCH, MANAGED_AGENT_MARKER, MANAGED_AGENT_NAME, USE_GLOBAL_PACKS_DEFAULT, PROJECT_FIELDS, bound, projectConfigRepository;
2393
2690
  var init_project_config = __esm({
2394
2691
  "src/config/project-config.ts"() {
2395
2692
  "use strict";
2396
- fs2 = __toESM(require("fs"));
2397
- path4 = __toESM(require("path"));
2693
+ fs3 = __toESM(require("fs"));
2694
+ path5 = __toESM(require("path"));
2398
2695
  YAML = __toESM(require("yaml"));
2399
2696
  import_zod4 = require("zod");
2400
2697
  init_fs();
2698
+ init_defaults();
2699
+ init_git_source();
2401
2700
  init_yaml();
2402
2701
  init_fs();
2403
2702
  init_errors();
@@ -2546,6 +2845,8 @@ var init_project_config = __esm({
2546
2845
  }
2547
2846
  };
2548
2847
  PART_EPOCH = "1970-01-01T00:00:00.000Z";
2848
+ MANAGED_AGENT_MARKER = "wairon:managed";
2849
+ MANAGED_AGENT_NAME = /-(owner|implementer|architect)\.(md|ya?ml)$/;
2549
2850
  USE_GLOBAL_PACKS_DEFAULT = ProjectConfigSchema.shape.extensions.unwrap().parse({}).useGlobalPacks;
2550
2851
  PROJECT_FIELDS = ["id", "members", "externals", "composition", "rules", "extensions", "targets"];
2551
2852
  bound = () => {
@@ -2643,12 +2944,12 @@ var init_project_config = __esm({
2643
2944
 
2644
2945
  // src/config/paths.ts
2645
2946
  function aiPathsAt(rootDir) {
2646
- const resolvedRoot = path5.resolve(rootDir);
2947
+ const resolvedRoot = path6.resolve(rootDir);
2647
2948
  const aiDirAt2 = (...segments) => {
2648
- const waiPath = path5.join(resolvedRoot, ".wai");
2649
- const waironPath = path5.join(resolvedRoot, ".wairon");
2650
- const base = !fs3.existsSync(waiPath) && fs3.existsSync(waironPath) ? waironPath : waiPath;
2651
- return path5.join(base, ...segments);
2949
+ const waiPath = path6.join(resolvedRoot, ".wai");
2950
+ const waironPath = path6.join(resolvedRoot, ".wairon");
2951
+ const base = !fs4.existsSync(waiPath) && fs4.existsSync(waironPath) ? waironPath : waiPath;
2952
+ return path6.join(base, ...segments);
2652
2953
  };
2653
2954
  const specsDir = () => projectConfigRepositoryAt(resolvedRoot).specsDir();
2654
2955
  return {
@@ -2665,12 +2966,12 @@ function aiPathsAt(rootDir) {
2665
2966
  contextDomainsMd: () => aiDirAt2("context", "domains.md"),
2666
2967
  contextWaironGuideMd: () => aiDirAt2("context", "wairon-guide.md"),
2667
2968
  specsDir,
2668
- specsSystem: () => path5.join(specsDir(), ".index.yaml"),
2669
- specsSubsystemsDir: () => path5.join(specsDir(), "subsystems"),
2670
- specsComponentsDir: () => path5.join(specsDir(), "components"),
2671
- specsInterfacesDir: () => path5.join(specsDir(), "interfaces"),
2672
- specsImplementationsDir: () => path5.join(specsDir(), "implementations"),
2673
- specsTypesDir: () => path5.join(specsDir(), "types")
2969
+ specsSystem: () => path6.join(specsDir(), ".index.yaml"),
2970
+ specsSubsystemsDir: () => path6.join(specsDir(), "subsystems"),
2971
+ specsComponentsDir: () => path6.join(specsDir(), "components"),
2972
+ specsInterfacesDir: () => path6.join(specsDir(), "interfaces"),
2973
+ specsImplementationsDir: () => path6.join(specsDir(), "implementations"),
2974
+ specsTypesDir: () => path6.join(specsDir(), "types")
2674
2975
  };
2675
2976
  }
2676
2977
  function isProjectInitialized() {
@@ -2681,12 +2982,12 @@ function assertProjectInitialized() {
2681
2982
  throw new ProjectNotInitializedError();
2682
2983
  }
2683
2984
  }
2684
- var fs3, path5, AI_PATHS;
2985
+ var fs4, path6, AI_PATHS;
2685
2986
  var init_paths = __esm({
2686
2987
  "src/config/paths.ts"() {
2687
2988
  "use strict";
2688
- fs3 = __toESM(require("fs"));
2689
- path5 = __toESM(require("path"));
2989
+ fs4 = __toESM(require("fs"));
2990
+ path6 = __toESM(require("path"));
2690
2991
  init_fs();
2691
2992
  init_errors();
2692
2993
  init_project_config();
@@ -2755,7 +3056,7 @@ function digestTree(keep, asStored = (impl) => impl) {
2755
3056
  implementations: kind(loadImplementationSpecs().map(asStored)),
2756
3057
  types: kind(loadTypeSpecs())
2757
3058
  };
2758
- const digest3 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
3059
+ const digest3 = crypto2.createHash("sha256").update(canonicalize(tree)).digest("hex");
2759
3060
  return { algorithm: CONTENT_ALGORITHM, digest: digest3 };
2760
3061
  }
2761
3062
  function computeStateId() {
@@ -2775,11 +3076,11 @@ function computeOwnStateId() {
2775
3076
  return digestTree((id) => (owners.get(id) ?? "") === "", asStored);
2776
3077
  }
2777
3078
  function isInside(dir, file) {
2778
- const rel2 = path6.relative(dir, file);
2779
- return rel2 !== "" && !rel2.startsWith("..") && !path6.isAbsolute(rel2);
3079
+ const rel2 = path7.relative(dir, file);
3080
+ return rel2 !== "" && !rel2.startsWith("..") && !path7.isAbsolute(rel2);
2780
3081
  }
2781
3082
  function fromPartRoot(impl, root, partDir) {
2782
- const from = (p) => path6.isAbsolute(p) ? p : path6.relative(partDir, path6.resolve(root, p)).split(path6.sep).join("/");
3083
+ const from = (p) => path7.isAbsolute(p) ? p : path7.relative(partDir, path7.resolve(root, p)).split(path7.sep).join("/");
2783
3084
  return {
2784
3085
  ...impl,
2785
3086
  ...impl.sourcePath ? { sourcePath: from(impl.sourcePath) } : {},
@@ -2790,13 +3091,13 @@ function fromPartRoot(impl, root, partDir) {
2790
3091
  function stateIdEquals(a, b) {
2791
3092
  return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
2792
3093
  }
2793
- var crypto, path6, CONTENT_ALGORITHM;
3094
+ var crypto2, path7, CONTENT_ALGORITHM;
2794
3095
  var init_statehash = __esm({
2795
3096
  "src/core/statehash.ts"() {
2796
3097
  "use strict";
2797
- crypto = __toESM(require("crypto"));
3098
+ crypto2 = __toESM(require("crypto"));
2798
3099
  init_specs2();
2799
- path6 = __toESM(require("path"));
3100
+ path7 = __toESM(require("path"));
2800
3101
  init_fs();
2801
3102
  init_canonical_json();
2802
3103
  CONTENT_ALGORITHM = "sha256";
@@ -2817,14 +3118,14 @@ function normalizeApprover(value) {
2817
3118
  }
2818
3119
  function readLockRecord() {
2819
3120
  try {
2820
- return normalizeRecord(JSON.parse(fs4.readFileSync(lockPath(), "utf8")));
3121
+ return normalizeRecord(JSON.parse(fs5.readFileSync(lockPath(), "utf8")));
2821
3122
  } catch {
2822
3123
  return null;
2823
3124
  }
2824
3125
  }
2825
3126
  function readLockRecordAt(root) {
2826
3127
  try {
2827
- return normalizeRecord(JSON.parse(fs4.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
3128
+ return normalizeRecord(JSON.parse(fs5.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
2828
3129
  } catch {
2829
3130
  return null;
2830
3131
  }
@@ -2835,10 +3136,10 @@ function normalizeRecord(raw) {
2835
3136
  }
2836
3137
  function writeLockRecord(record2) {
2837
3138
  const p = lockPath();
2838
- fs4.mkdirSync(path7.dirname(p), { recursive: true });
3139
+ fs5.mkdirSync(path8.dirname(p), { recursive: true });
2839
3140
  const tmp = `${p}.tmp`;
2840
- fs4.writeFileSync(tmp, JSON.stringify(withSortedMaps(record2), null, 2) + "\n");
2841
- fs4.renameSync(tmp, p);
3141
+ fs5.writeFileSync(tmp, withLineEndings(p, JSON.stringify(withSortedMaps(record2), null, 2) + "\n"));
3142
+ fs5.renameSync(tmp, p);
2842
3143
  }
2843
3144
  function withSortedMaps(record2) {
2844
3145
  const sorted = (m) => {
@@ -2856,12 +3157,12 @@ function withSortedMaps(record2) {
2856
3157
  ...members ? { members } : {}
2857
3158
  };
2858
3159
  }
2859
- var fs4, path7;
3160
+ var fs5, path8;
2860
3161
  var init_lockfile = __esm({
2861
3162
  "src/core/lockfile.ts"() {
2862
3163
  "use strict";
2863
- fs4 = __toESM(require("fs"));
2864
- path7 = __toESM(require("path"));
3164
+ fs5 = __toESM(require("fs"));
3165
+ path8 = __toESM(require("path"));
2865
3166
  init_fs();
2866
3167
  }
2867
3168
  });
@@ -2877,157 +3178,31 @@ function listSpecFiles(specsDir) {
2877
3178
  return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
2878
3179
  }
2879
3180
  function removeSpecFile(specPath, specsRoot) {
2880
- if (!fs5.existsSync(specPath)) return false;
2881
- fs5.unlinkSync(specPath);
2882
- pruneEmptyDirs(path8.dirname(specPath), path8.resolve(specsRoot));
3181
+ if (!fs6.existsSync(specPath)) return false;
3182
+ fs6.unlinkSync(specPath);
3183
+ pruneEmptyDirs(path9.dirname(specPath), path9.resolve(specsRoot));
2883
3184
  return true;
2884
3185
  }
2885
3186
  function pruneEmptyDirs(dir, specsRoot) {
2886
3187
  let at = dir;
2887
3188
  while (at !== specsRoot && at.startsWith(specsRoot)) {
2888
- if (!fs5.existsSync(at) || fs5.readdirSync(at).length > 0) return;
2889
- fs5.rmdirSync(at);
2890
- at = path8.dirname(at);
3189
+ if (!fs6.existsSync(at) || fs6.readdirSync(at).length > 0) return;
3190
+ fs6.rmdirSync(at);
3191
+ at = path9.dirname(at);
2891
3192
  }
2892
3193
  }
2893
- var fs5, path8, SPEC_FILE_EXTENSION;
3194
+ var fs6, path9, SPEC_FILE_EXTENSION;
2894
3195
  var init_spec_files = __esm({
2895
3196
  "src/core/spec-files.ts"() {
2896
3197
  "use strict";
2897
- fs5 = __toESM(require("fs"));
2898
- path8 = __toESM(require("path"));
3198
+ fs6 = __toESM(require("fs"));
3199
+ path9 = __toESM(require("path"));
2899
3200
  init_fs();
2900
3201
  init_yaml();
2901
3202
  SPEC_FILE_EXTENSION = ".yaml";
2902
3203
  }
2903
3204
  });
2904
3205
 
2905
- // src/core/adapters/git-source.ts
2906
- function osUserCache() {
2907
- if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path9.join(os2.homedir(), "AppData", "Local");
2908
- if (process.platform === "darwin") return path9.join(os2.homedir(), "Library", "Caches");
2909
- return process.env.XDG_CACHE_HOME ?? path9.join(os2.homedir(), ".cache");
2910
- }
2911
- function cacheRoot() {
2912
- const setting = process.env.WAIRON_CACHE_DIR ?? "";
2913
- let root = cacheRoots.get(setting);
2914
- if (root === void 0) {
2915
- root = setting !== "" ? path9.resolve(setting) : path9.join(osUserCache(), "wairon");
2916
- cacheRoots.set(setting, root);
2917
- }
2918
- return root;
2919
- }
2920
- function git(args, cwd) {
2921
- return (0, import_child_process.execFileSync)("git", args, {
2922
- cwd,
2923
- encoding: "utf8",
2924
- stdio: ["ignore", "pipe", "pipe"],
2925
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
2926
- });
2927
- }
2928
- function reasonOf(e) {
2929
- const stderr = e?.stderr;
2930
- const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
2931
- return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
2932
- }
2933
- function cacheEntryOf(url, commit3) {
2934
- const repo = crypto2.createHash("sha256").update(url).digest("hex");
2935
- return path9.join(cacheRoot(), "git", repo, commit3);
2936
- }
2937
- function within(entry, url, commit3, dir) {
2938
- const at = dir ? path9.join(entry, dir) : entry;
2939
- if (!fs6.existsSync(at)) {
2940
- throw new SourceUnavailable(`the commit ${commit3} of ${url} holds no "${dir}"`);
2941
- }
2942
- return at;
2943
- }
2944
- function fetchInto(tmp, url, commit3) {
2945
- git(["init", "-q"], tmp);
2946
- try {
2947
- git(["fetch", "-q", "--depth", "1", url, commit3], tmp);
2948
- } catch {
2949
- git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
2950
- }
2951
- git(["-c", "advice.detachedHead=false", "checkout", "-q", commit3], tmp);
2952
- fs6.rmSync(path9.join(tmp, ".git"), { recursive: true, force: true });
2953
- }
2954
- function fetch2(url, commit3, dir) {
2955
- const entry = cacheEntryOf(url, commit3);
2956
- if (fs6.existsSync(entry)) return within(entry, url, commit3, dir);
2957
- fs6.mkdirSync(path9.dirname(entry), { recursive: true });
2958
- const tmp = `${entry}.tmp-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
2959
- fs6.mkdirSync(tmp, { recursive: true });
2960
- try {
2961
- fetchInto(tmp, url, commit3);
2962
- try {
2963
- fs6.renameSync(tmp, entry);
2964
- } catch (e) {
2965
- if (!fs6.existsSync(entry)) throw e;
2966
- fs6.rmSync(tmp, { recursive: true, force: true });
2967
- }
2968
- } catch (e) {
2969
- fs6.rmSync(tmp, { recursive: true, force: true });
2970
- if (fs6.readdirSync(path9.dirname(entry)).length === 0) fs6.rmdirSync(path9.dirname(entry));
2971
- throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit3} of ${url}: ${reasonOf(e)}`);
2972
- }
2973
- return within(entry, url, commit3, dir);
2974
- }
2975
- function resolve7(url, ref) {
2976
- let listed;
2977
- try {
2978
- listed = git(["ls-remote", url, ref ?? "HEAD"]);
2979
- } catch (e) {
2980
- throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
2981
- }
2982
- const wanted = commitListed(listed, ref ?? "HEAD");
2983
- if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
2984
- return wanted;
2985
- }
2986
- function commitListed(listed, ref) {
2987
- const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
2988
- const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
2989
- for (const name of names) {
2990
- const line2 = lines.find(([, listedName]) => listedName === name);
2991
- if (line2) return line2[0];
2992
- }
2993
- return void 0;
2994
- }
2995
- function head(directory) {
2996
- try {
2997
- return git(["rev-parse", "HEAD"], directory).trim() || null;
2998
- } catch {
2999
- return null;
3000
- }
3001
- }
3002
- function repositoryRoot(directory) {
3003
- let at = path9.resolve(directory);
3004
- for (; ; ) {
3005
- if (fs6.existsSync(path9.join(at, ".git"))) return at;
3006
- const up = path9.dirname(at);
3007
- if (up === at) return null;
3008
- at = up;
3009
- }
3010
- }
3011
- var import_child_process, crypto2, fs6, os2, path9, SourceUnavailable, cacheRoots;
3012
- var init_git_source = __esm({
3013
- "src/core/adapters/git-source.ts"() {
3014
- "use strict";
3015
- import_child_process = require("child_process");
3016
- crypto2 = __toESM(require("crypto"));
3017
- fs6 = __toESM(require("fs"));
3018
- os2 = __toESM(require("os"));
3019
- path9 = __toESM(require("path"));
3020
- init_errors();
3021
- SourceUnavailable = class extends WaironError {
3022
- constructor(message) {
3023
- super(message);
3024
- this.name = "SourceUnavailable";
3025
- }
3026
- };
3027
- cacheRoots = /* @__PURE__ */ new Map();
3028
- }
3029
- });
3030
-
3031
3206
  // src/models/domain.ts
3032
3207
  function createEmptyTopologyConfig() {
3033
3208
  return { schemaVersion: "1.0.0", domains: [] };
@@ -4459,7 +4634,7 @@ function optionalOf(inner2) {
4459
4634
  return { form: "optional", args: [inner2] };
4460
4635
  }
4461
4636
  function inner(ctx) {
4462
- return { ...ctx, top: false, voidAllowed: false, omittable: false };
4637
+ return { ...ctx, top: false, voidAllowed: false, resultAllowed: false, omittable: false };
4463
4638
  }
4464
4639
  function normalise(node, ctx, notes) {
4465
4640
  switch (node.k) {
@@ -4493,9 +4668,21 @@ function asyncOf(innerNode, ctx, notes, spelled) {
4493
4668
  if (!ctx.top || !RETURNS_POSITIONS.has(ctx.position)) {
4494
4669
  throw new TypeProblemError("TYPE_POSITION_INVALID", `${spelled} may stand only at the top of a returns, not in a ${ctx.position}`);
4495
4670
  }
4496
- const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true }, notes);
4671
+ const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true, resultAllowed: true }, notes);
4497
4672
  return { form: "async", args: [read2] };
4498
4673
  }
4674
+ function resultOf(args, ctx, notes, spelled) {
4675
+ if (!ctx.resultAllowed) {
4676
+ throw new TypeProblemError(
4677
+ "TYPE_POSITION_INVALID",
4678
+ `${spelled} may stand only as a whole returns (or under its async), not in a ${ctx.top ? ctx.position : "collection, union, optional or argument"}`,
4679
+ "the success type here (a failure belongs to a method's returns)"
4680
+ );
4681
+ }
4682
+ const ok = normalise(args[0], { ...inner(ctx), voidAllowed: true }, notes);
4683
+ const err = normalise(args[1], inner(ctx), notes);
4684
+ return { form: "result", args: [ok, err] };
4685
+ }
4499
4686
  function bareName(name, ctx, notes) {
4500
4687
  const lower = name.toLowerCase();
4501
4688
  if (NONES.has(name)) {
@@ -4533,6 +4720,7 @@ function appliedName(name, args, ctx, notes) {
4533
4720
  throw new TypeProblemError("TYPE_EXPRESSION_INVALID", `"${name}" takes ${alias.arity} type argument${alias.arity === 1 ? "" : "s"}, ${args.length} given`);
4534
4721
  }
4535
4722
  if (alias.form === "async") return asyncOf(args[0], ctx, notes, `${name}<...>`);
4723
+ if (alias.form === "result") return resultOf(args, ctx, notes, `${name}<...>`);
4536
4724
  if (alias.form === "optional") return optionalOf(normalise(args[0], inner(ctx), notes));
4537
4725
  const read2 = args.map((a) => normalise(a, inner(ctx), notes));
4538
4726
  if (alias.form === "map") assertMapKey(read2[0], args[0]);
@@ -4606,6 +4794,8 @@ function canonicalTypeText(expr) {
4606
4794
  return `${expr.form}<${canonicalTypeText(expr.args[0])}>`;
4607
4795
  case "map":
4608
4796
  return `map<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
4797
+ case "result":
4798
+ return `result<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
4609
4799
  case "optional": {
4610
4800
  const innerText = canonicalTypeText(expr.args[0]);
4611
4801
  return expr.args[0].form === "union" ? `(${innerText})?` : `${innerText}?`;
@@ -4625,11 +4815,12 @@ function typeNamedRefs(expr) {
4625
4815
  }
4626
4816
  function typeIsMany(expr) {
4627
4817
  if (expr.form === "list" || expr.form === "set" || expr.form === "map") return true;
4628
- if (expr.form === "optional" || expr.form === "async") return typeIsMany(expr.args[0]);
4818
+ if (expr.form === "optional" || expr.form === "async" || expr.form === "result") return typeIsMany(expr.args[0]);
4629
4819
  return false;
4630
4820
  }
4631
4821
  function contextFor(position, omittable2, looseNumber = false) {
4632
- return { position, top: true, voidAllowed: RETURNS_POSITIONS.has(position), omittable: omittable2, looseNumber };
4822
+ const returns = RETURNS_POSITIONS.has(position);
4823
+ return { position, top: true, voidAllowed: returns, resultAllowed: returns, omittable: omittable2, looseNumber };
4633
4824
  }
4634
4825
  function parseTypePosition(text3, position, omittable2 = false) {
4635
4826
  let raw;
@@ -4754,6 +4945,47 @@ function typeProblemIntProposal(problem) {
4754
4945
  if (!parse.expression || parse.problem) return null;
4755
4946
  return { specId: problem.specId, kind: problem.kind, path: problem.path, written: problem.written, stored: parse.canonical };
4756
4947
  }
4948
+ function stringLiteralValue(text3) {
4949
+ const quote = text3[0];
4950
+ if (quote !== "'" && quote !== '"' && quote !== "`" || text3[text3.length - 1] !== quote) return null;
4951
+ return text3.slice(1, -1).replace(/\\(.)/g, "$1");
4952
+ }
4953
+ function kebabOf(name) {
4954
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_.]+/g, "-").toLowerCase();
4955
+ }
4956
+ function typeProblemEnumProposal(problem) {
4957
+ if (problem.code !== "TYPE_FORM_UNSUPPORTED") return null;
4958
+ if (problem.specId === void 0 || problem.kind === void 0 || problem.path === void 0) return null;
4959
+ let raw;
4960
+ try {
4961
+ raw = readRaw(problem.written);
4962
+ } catch (e) {
4963
+ if (e instanceof TypeSyntaxError) return null;
4964
+ throw e;
4965
+ }
4966
+ const members = raw.k === "union" ? raw.members : [raw];
4967
+ const values = [];
4968
+ let optional = false;
4969
+ for (const member of members) {
4970
+ if (member.k === "name" && member.args === void 0 && NONES.has(member.name)) {
4971
+ optional = true;
4972
+ continue;
4973
+ }
4974
+ const value = member.k === "literal" ? stringLiteralValue(member.text) : null;
4975
+ if (value === null) return null;
4976
+ if (!values.includes(value)) values.push(value);
4977
+ }
4978
+ if (values.length === 0) return null;
4979
+ return {
4980
+ specId: problem.specId,
4981
+ kind: problem.kind,
4982
+ path: problem.path,
4983
+ written: problem.written,
4984
+ enumId: kebabOf(positionNameOf(problem.path)),
4985
+ values,
4986
+ optional
4987
+ };
4988
+ }
4757
4989
  function emptyTypeSpellingFacts() {
4758
4990
  return { respellings: [], problems: [] };
4759
4991
  }
@@ -4812,7 +5044,11 @@ var init_type_grammar = __esm({
4812
5044
  ["HashMap", { form: "map", arity: 2 }],
4813
5045
  ["Option", { form: "optional", arity: 1 }],
4814
5046
  ["Optional", { form: "optional", arity: 1 }],
4815
- ["Promise", { form: "async", arity: 1 }]
5047
+ ["Promise", { form: "async", arity: 1 }],
5048
+ // The success-or-failure type the mainstream languages with typed failures
5049
+ // share (Rust, Swift, Kotlin, F#); Rust's spelling is its alias.
5050
+ ["result", { form: "result", arity: 2 }],
5051
+ ["Result", { form: "result", arity: 2 }]
4816
5052
  ]);
4817
5053
  NOT_NEUTRAL = /* @__PURE__ */ new Map([
4818
5054
  ["uuid", "a named value-object holding a string (e.g. order_id)"],
@@ -4821,7 +5057,6 @@ var init_type_grammar = __esm({
4821
5057
  ["byte", "int, or bytes for binary data"],
4822
5058
  ["time", "datetime for an instant, or duration for an elapsed time"],
4823
5059
  ["tuple", "a named value-object"],
4824
- ["result", "the success type as the returns (failures as findings or throws), or a named value-object"],
4825
5060
  ["error", "a named value-object"],
4826
5061
  ["never", "void"],
4827
5062
  ["box", "the inner type (ownership is an implementation detail)"],
@@ -4964,9 +5199,9 @@ var init_type_grammar = __esm({
4964
5199
  }
4965
5200
  name += this.ident();
4966
5201
  while (this.isP("::") || this.isP(".")) {
4967
- const sep18 = this.peek().v;
5202
+ const sep19 = this.peek().v;
4968
5203
  this.at++;
4969
- name += sep18 + this.ident();
5204
+ name += sep19 + this.ident();
4970
5205
  }
4971
5206
  if (!this.isP("<")) return { k: "name", name };
4972
5207
  this.at++;
@@ -4993,6 +5228,10 @@ var init_type_grammar = __esm({
4993
5228
  const close2 = this.closingParen();
4994
5229
  const after = close2 === -1 ? void 0 : this.tokens[close2 + 1];
4995
5230
  if (!after || after.t !== "p" || after.v !== "=>") {
5231
+ if (this.isP(")", 1)) {
5232
+ this.at += 2;
5233
+ return { k: "name", name: "void" };
5234
+ }
4996
5235
  this.expect("(");
4997
5236
  const inner2 = this.type();
4998
5237
  this.expect(")");
@@ -5309,6 +5548,7 @@ function lastSegment(name) {
5309
5548
  return name.split(/::|\./).filter(Boolean).pop() ?? name;
5310
5549
  }
5311
5550
  function sameType(code, spec, codeNames) {
5551
+ if (spec.form === "result" && code.form !== "result") return sameType(code, spec.args[0], codeNames);
5312
5552
  if (code.form === "primitive" && code.name === "number") return spec.form === "primitive" && (spec.name === "int" || spec.name === "float");
5313
5553
  if (code.form === "named" && (spec.form === "named" || spec.form === "applied")) {
5314
5554
  if (spec.form === "applied") return false;
@@ -5345,6 +5585,8 @@ function writeTypeScript(expr) {
5345
5585
  return expr.args.map(writeTypeScript).join(" | ");
5346
5586
  case "async":
5347
5587
  return `Promise<${writeTypeScript(expr.args[0])}>`;
5588
+ case "result":
5589
+ return writeTypeScript(expr.args[0]);
5348
5590
  case "applied":
5349
5591
  return `${expr.name}<${expr.args.map(writeTypeScript).join(", ")}>`;
5350
5592
  }
@@ -5395,6 +5637,7 @@ var init_type_dialects = __esm({
5395
5637
  "T? \u2192 T | null",
5396
5638
  "A | B \u2192 A | B",
5397
5639
  "async T \u2192 Promise<T>",
5640
+ "result<T, E> \u2192 T, throwing E on failure (result<void, E> \u2192 void)",
5398
5641
  "an enum E \u2192 type E = 'a' | 'b' (a string-literal union alias)",
5399
5642
  "a named scalar E holding P \u2192 type E = P (an alias of the primitive)"
5400
5643
  ];
@@ -6147,7 +6390,7 @@ var init_design_export = __esm({
6147
6390
  ExtSchema = import_zod9.z.record(import_zod9.z.unknown());
6148
6391
  TypeExpressionSchema = import_zod9.z.lazy(
6149
6392
  () => import_zod9.z.object({
6150
- form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied"]),
6393
+ form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied", "result"]),
6151
6394
  name: import_zod9.z.string().optional(),
6152
6395
  args: import_zod9.z.array(TypeExpressionSchema)
6153
6396
  }).passthrough()
@@ -11615,7 +11858,7 @@ function snapshotPath(root, alias) {
11615
11858
  function writeAtomically(file, text3) {
11616
11859
  fs8.mkdirSync(path13.dirname(file), { recursive: true });
11617
11860
  const tmp = `${file}.${process.pid}.tmp`;
11618
- fs8.writeFileSync(tmp, text3);
11861
+ fs8.writeFileSync(tmp, withLineEndings(file, text3));
11619
11862
  fs8.renameSync(tmp, file);
11620
11863
  }
11621
11864
  function sortedLock(lock) {
@@ -11790,6 +12033,8 @@ function schemaOfExpression(expr, closureIds) {
11790
12033
  return { oneOf: expr.args.map((member) => schemaOfExpression(member, closureIds)) };
11791
12034
  case "async":
11792
12035
  return schemaOfExpression(expr.args[0], closureIds);
12036
+ case "result":
12037
+ return schemaOfExpression(expr.args[0], closureIds);
11793
12038
  }
11794
12039
  }
11795
12040
  function schemaFor(typeRef2, closureIds) {
@@ -11799,7 +12044,8 @@ function schemaFor(typeRef2, closureIds) {
11799
12044
  }
11800
12045
  function returnsNothing(returns) {
11801
12046
  const expr = expressionOf(returns);
11802
- const inner2 = expr?.form === "async" ? expr.args[0] : expr;
12047
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
12048
+ const inner2 = awaited?.form === "result" ? awaited.args[0] : awaited;
11803
12049
  return inner2?.form === "primitive" && inner2.name === "void";
11804
12050
  }
11805
12051
  function enumDescription(values) {
@@ -13795,7 +14041,7 @@ function approvalFinding(entry, config) {
13795
14041
  return finding(
13796
14042
  config,
13797
14043
  "MEMBER_DRIFTED",
13798
- `The member ${named(entry.key)} was approved under an earlier gate identity \u2014 the gate identity gained inputs in stage 5 (members' composition subjects, \`composition\`; code conformance moved beside the claim), which says nothing about its design. Re-lock it once at its own root (\`wairon lock\`).`,
14044
+ `The member ${named(entry.key)} was approved under an earlier gate identity \u2014 the gate identity gained inputs in this release (members' composition subjects, \`composition\`; code conformance moved beside the claim), which says nothing about its design. Re-lock it once at its own root (\`wairon lock\`).`,
13799
14045
  entry.key
13800
14046
  );
13801
14047
  }
@@ -14689,6 +14935,16 @@ function verifyIntegrity(selection, packDir, where) {
14689
14935
  message: `Pack "${wanted}" resolved to content that does not match the pinned integrity: expected ${selection.integrity}, found ${actual} (${where}). The pack was modified after it was pinned \u2014 reinstall or re-bundle the pinned version, or update the pin with \`wairon pack use ${selection.name} --pin\` if the change is intended.`
14690
14936
  };
14691
14937
  }
14938
+ function bundlesSingleFile(bundleDir, storePath16) {
14939
+ try {
14940
+ if (!fs12.statSync(storePath16).isFile()) return false;
14941
+ const entry = packDirEntry(bundleDir);
14942
+ if (!entry || fs12.readdirSync(bundleDir).length !== 1) return false;
14943
+ return fs12.readFileSync(entry).equals(fs12.readFileSync(storePath16));
14944
+ } catch {
14945
+ return false;
14946
+ }
14947
+ }
14692
14948
  function detectBundleStoreDrift(selection, bundleDir) {
14693
14949
  const bundledVersion = path17.basename(bundleDir);
14694
14950
  const installed = resolveInstalledPack(selection.name, bundledVersion);
@@ -14696,6 +14952,7 @@ function detectBundleStoreDrift(selection, bundleDir) {
14696
14952
  const bundleDigest = computePackDigest(bundleDir);
14697
14953
  const storeDigest = computePackDigest(installed.path);
14698
14954
  if (bundleDigest === storeDigest) return void 0;
14955
+ if (bundlesSingleFile(bundleDir, installed.path)) return void 0;
14699
14956
  return {
14700
14957
  code: "PACK_STORE_DRIFT",
14701
14958
  severity: "warning",
@@ -15449,7 +15706,7 @@ function judgeOwnIdentity(ctx, identity2) {
15449
15706
  ctx.addIssue(
15450
15707
  "notice",
15451
15708
  "PROJECT_ID_DEFAULTED",
15452
- `Project "${identity2.name}" declares no id in .wai/project.yaml, so it answers to "${identity2.id}", derived from its display name \u2014 renaming the project would move it. No save writes it for you; declare it with \`id: ${identity2.id}\` (stage 3 requires an explicit id).`
15709
+ `Project "${identity2.name}" declares no id in .wai/project.yaml, so it answers to "${identity2.id}", derived from its display name \u2014 renaming the project would move it. No save writes it for you; declare it with \`id: ${identity2.id}\` (a project is keyed on an explicit id).`
15453
15710
  );
15454
15711
  }
15455
15712
  const ambiguous = problem("ambiguous");
@@ -15483,7 +15740,7 @@ var init_project_identity = __esm({
15483
15740
  projectIdentityRule = {
15484
15741
  name: "project-identity",
15485
15742
  judges: "design",
15486
- description: "The bound project is keyed on its id, so the id must be declared, well-formed, stable and its own: a project that declares none answers to its display name slugified until it writes one (PROJECT_ID_DEFAULTED), a project whose name yields no id or whose declared id breaks the grammar has nothing reliable to key on (PROJECT_ID_AMBIGUOUS), an id the rename migration moved from the one the lock approved is owed a re-lock and nothing more (PROJECT_ID_RENAMED: the approved id is one of the configuration's previousIds), and any other id that differs from the one the lock approved has moved under everything that keyed on it (PROJECT_ID_CHANGED). It judges the bound project's own identity only. The family half \u2014 a member with a defaulted id (named with its alias as the id to declare), a member with no id, and two members resolving to one id \u2014 is a family check since stage 4 (family_validator.checkMembers), which reads the family root's graph.",
15743
+ description: "The bound project is keyed on its id, so the id must be declared, well-formed, stable and its own: a project that declares none answers to its display name slugified until it writes one (PROJECT_ID_DEFAULTED), a project whose name yields no id or whose declared id breaks the grammar has nothing reliable to key on (PROJECT_ID_AMBIGUOUS), an id the rename migration moved from the one the lock approved is owed a re-lock and nothing more (PROJECT_ID_RENAMED: the approved id is one of the configuration's previousIds), and any other id that differs from the one the lock approved has moved under everything that keyed on it (PROJECT_ID_CHANGED). It judges the bound project's own identity only. The family half \u2014 a member with a defaulted id (named with its alias as the id to declare), a member with no id, and two members resolving to one id \u2014 is a family check (family_validator.checkMembers), which reads the family root's graph.",
15487
15744
  codes: [
15488
15745
  { code: "PROJECT_ID_AMBIGUOUS", defaultSeverity: "warning", summary: "Project has no usable id: its name yields no slug, or its declared id breaks the grammar" },
15489
15746
  { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" },
@@ -15909,10 +16166,10 @@ var init_type_expressions = __esm({
15909
16166
  typeExpressionsRule = {
15910
16167
  name: "type-expressions",
15911
16168
  judges: "design",
15912
- description: "Reports what the scan's type canonicalisation recorded (ctx.typeSpellingFacts), one finding per position, located by spec and path: a stored text that does not parse (TYPE_EXPRESSION_INVALID); one that breaks a position rule \u2014 void or async out of place, a map key that is not string, int or an enum, a named scalar's holds that is not one primitive other than void and any, `T??` (TYPE_POSITION_INVALID); one using a form the grammar leaves out \u2014 an inline object shape, an inline function type, a string-literal union, a union mixing in a primitive or a collection, an intersection, a utility type or a tuple (TYPE_FORM_UNSUPPORTED); one naming `number` or a legacy builtin with no neutral meaning (TYPE_NOT_NEUTRAL); and one that is an alias of its canonical spelling (TYPE_SPELLING_STALE, which any save or doctor --fix repairs). Every message names the replacement: `number` asks \"int or float?\" (and says when the doctor repair proposes int), a function type names a signature type, a literal union an enum, an inline object or a mixed union a named value-object. The writer refuses all but the stale spelling at write time; these findings are how a tree that held them before the grammar existed is told, while it still loads and its consumers read the position as opaque any \u2014 which is why only what no real tree holds (a text that does not parse, a broken position rule) is an error. No finding when the run carries no facts (a candidate run).",
16169
+ description: "Reports what the scan's type canonicalisation recorded (ctx.typeSpellingFacts), one finding per position, located by spec and path: a stored text that does not parse (TYPE_EXPRESSION_INVALID); one that breaks a position rule \u2014 void, async or result out of place, a map key that is not string, int or an enum, a named scalar's holds that is not one primitive other than void and any, `T??` (TYPE_POSITION_INVALID); one using a form the grammar leaves out \u2014 an inline object shape, an inline function type, a string-literal union, a union mixing in a primitive or a collection, an intersection, a utility type or a tuple (TYPE_FORM_UNSUPPORTED); one naming `number` or a legacy builtin with no neutral meaning (TYPE_NOT_NEUTRAL); and one that is an alias of its canonical spelling (TYPE_SPELLING_STALE, which any save or doctor --fix repairs). Every message names the replacement: `number` asks \"int or float?\" (and says when the doctor repair proposes int), a function type names a signature type, a literal union an enum (which the doctor repair proposes), an inline object or a mixed union a named value-object. The writer refuses all but the stale spelling at write time; on load every one of them is a warning \u2014 these findings are how a tree that held them before the grammar existed is told, while it still loads, stays lockable with its debt in view, and its consumers read the position as opaque any, which never invents a shape. No finding when the run carries no facts (a candidate run).",
15913
16170
  codes: [
15914
- { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "error", summary: "A structured type position does not parse under the type grammar" },
15915
- { code: "TYPE_POSITION_INVALID", defaultSeverity: "error", summary: "A type position breaks a position rule: void or async out of place, a non-scalar map key, or T??" },
16171
+ { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "warning", summary: "A structured type position does not parse under the type grammar" },
16172
+ { code: "TYPE_POSITION_INVALID", defaultSeverity: "warning", summary: "A type position breaks a position rule: void, async or result out of place, a non-scalar map key, or T??" },
15916
16173
  { code: "TYPE_FORM_UNSUPPORTED", defaultSeverity: "warning", summary: "A type position uses a form the grammar leaves out \u2014 inline object, inline function type, string-literal union, union mixing in a primitive or collection, intersection, utility type or tuple \u2014 and the message names its named replacement" },
15917
16174
  { code: "TYPE_NOT_NEUTRAL", defaultSeverity: "warning", summary: "A type position names number (int or float?) or a legacy builtin with no neutral meaning, and the message names the replacement" },
15918
16175
  { code: "TYPE_SPELLING_STALE", defaultSeverity: "warning", summary: "A stored type position is an alias of its canonical spelling; any save or doctor --fix rewrites it" }
@@ -15922,9 +16179,8 @@ var init_type_expressions = __esm({
15922
16179
  if (!facts) return;
15923
16180
  for (const problem of facts.problems) {
15924
16181
  if (problem.specId === void 0 || !ctx.isSpecInScope(problem.specId)) continue;
15925
- const severity = problem.code === "TYPE_EXPRESSION_INVALID" || problem.code === "TYPE_POSITION_INVALID" ? "error" : "warning";
15926
16182
  ctx.addIssue(
15927
- severity,
16183
+ "warning",
15928
16184
  problem.code,
15929
16185
  problemMessage(problem, problem.specId),
15930
16186
  problem.specId,
@@ -16313,7 +16569,7 @@ var init_export_tables = __esm({
16313
16569
  exportTablesRule = {
16314
16570
  name: "export-tables",
16315
16571
  judges: "design",
16316
- description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`: one public name binds one target (EXPORT_ID_DUPLICATE when two wildcards \u2014 or two explicit entries \u2014 bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out), no chain leads back to itself (EXPORT_CYCLE \u2014 an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union), every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns \u2014 a subsystem-owned type at L1, a project-level type (no subsystem) for an L0 `{ typeDef }` without `from` \u2014 under a public name of [a-z0-9-_]+ (EXPORT_INVALID), every re-exported component can serve a boundary caller \u2014 a Portal, a gateway being a Portal variant, or an Observer for events (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports; an error since stage 4), and an L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. The rule judges the facts the export resolver recorded in the scan \u2014 for the bound root's table and every contained member's \u2014 and resolves nothing itself.",
16572
+ description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`: one public name binds one target (EXPORT_ID_DUPLICATE when two wildcards \u2014 or two explicit entries \u2014 bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out), no chain leads back to itself (EXPORT_CYCLE \u2014 an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union), every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns \u2014 a subsystem-owned type at L1, a project-level type (no subsystem) for an L0 `{ typeDef }` without `from` \u2014 under a public name of [a-z0-9-_]+ (EXPORT_INVALID), every re-exported component can serve a boundary caller \u2014 a Portal, a gateway being a Portal variant, or an Observer for events (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports), and an L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. The rule judges the facts the export resolver recorded in the scan \u2014 for the bound root's table and every contained member's \u2014 and resolves nothing itself.",
16317
16573
  codes: [
16318
16574
  { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
16319
16575
  { code: "EXPORT_CYCLE", defaultSeverity: "error", summary: "A re-export chain leads back to itself (a warning for a wildcard cycle, which resolves to its union)" },
@@ -16489,7 +16745,7 @@ var init_reference_forms = __esm({
16489
16745
  referenceFormsRule = {
16490
16746
  name: "reference-forms",
16491
16747
  judges: "design",
16492
- description: "A cross-project reference is `alias::name` (or a bare name a `use` imports): the alias is one of the referring project's members or externals, the name a public name of that project's L0 table. The older forms still bind for one release and are reported (DEPRECATED_REFERENCE_FORM), each naming the spec, where in it, the form as written and the text to write instead (the scan's rewrite: a bare local id, or `alias::publicName`): a leading `::`, which escapes to the root of whatever checkout loads it; `super::`, which climbs the containing projects \u2014 a place in one family, not a name; a member path (`desktop::shell` written inside desktop, `waffler_core::transpiler::x` written inside transpiler), a first segment the referring project does not declare, read from the bound root; and a self-prefix \u2014 the referring project's own id as the first segment, with `::` or `.` (`registry.advisory-channel` inside registry), which stage 3's migration did not rewrite and stage 4's positional step does. Wairon's own writer emits none of them. A form with no rewrite (its target is out of reach) is reported with the reason. It reads the project graph's authored references, which the scan records before binding erases the form.",
16748
+ description: "A cross-project reference is `alias::name` (or a bare name a `use` imports): the alias is one of the referring project's members or externals, the name a public name of that project's L0 table. The older forms still bind for one release and are reported (DEPRECATED_REFERENCE_FORM), each naming the spec, where in it, the form as written and the text to write instead (the scan's rewrite: a bare local id, or `alias::publicName`): a leading `::`, which escapes to the root of whatever checkout loads it; `super::`, which climbs the containing projects \u2014 a place in one family, not a name; a member path (`desktop::shell` written inside desktop, `app_core::engine::x` written inside engine), a first segment the referring project does not declare, read from the bound root; and a self-prefix \u2014 the referring project's own id as the first segment, with `::` or `.` (`registry.advisory-channel` inside registry), which `wairon doctor --fix` rewrites to the bare local id. Wairon's own writer emits none of them. A form with no rewrite (its target is out of reach) is reported with the reason. It reads the project graph's authored references, which the scan records before binding erases the form.",
16493
16749
  codes: [
16494
16750
  { code: "DEPRECATED_REFERENCE_FORM", defaultSeverity: "notice", summary: "A reference written with a leading ::, super:: or a member path where a local id or alias::name belongs" }
16495
16751
  ],
@@ -16526,9 +16782,9 @@ var init_member_declarations = __esm({
16526
16782
  memberDeclarationsRule = {
16527
16783
  name: "member-declarations",
16528
16784
  judges: "design",
16529
- description: "A project declares its members in project.yaml `members`, each by one source in one grammar. A deprecated declaration form loads for one release and is reported (DEPRECATED_MOUNT_FORM, notice): the L1 form \u2014 a subsystem carrying projectPath \u2014 naming the mount, its path, the `members` entry that replaces it, and every field it carries that `doctor --fix` will move into the member (or stop on); a mount whose alias `members` also declares is reported as ignored; and (stage 8) the long-form `path` key, naming the `source` (or shorthand) `doctor --fix` rewrites it to. Since stage 8 it also judges what each member of the bound project IS against what is declared or written about it, from the scan's problems at the bound root (a member's own members are that member's gate's): what a member is follows from its content \u2014 an id, an L0 or a lock make it a project, anything else a part \u2014 and every contradiction is ONE finding, MEMBER_KIND_MISMATCH (error), naming the alias and the contradiction: an asserted `as` the content contradicts; a part whose configuration declares project fields (members, externals, rules, extensions, composition, targets) without the id that would make it a project (add the id, or move them to the parent); a non-contained part with no PartOf, or one naming another project (opened alone it could not say what it belongs to); a contained member declaring PartOf; a `use` on a member whose content is a part (its names are already this project's own). A part whose files could not be read \u2014 its directory absent, or its git commit not cached and not fetchable (offline, refused, unknown) \u2014 is PART_UNAVAILABLE (error): the project's own subsystems are missing, so its verdict cannot be a pass. The member findings name no spec: members live in configuration. It reads the project graph's nodes, their legacy mounts, the bound root's declared members and its problems.",
16785
+ description: "A project declares its members in project.yaml `members`, each by one source in one grammar. A deprecated declaration form loads for one release and is reported (DEPRECATED_MOUNT_FORM, notice): the L1 form \u2014 a subsystem carrying projectPath \u2014 naming the mount, its path, the `members` entry that replaces it, and every field it carries that `doctor --fix` will move into the member (or stop on); a mount whose alias `members` also declares is reported as ignored; and the deprecated long-form `path` key, naming the `source` (or shorthand) `doctor --fix` rewrites it to. It also judges what each member of the bound project IS against what is declared or written about it, from the scan's problems at the bound root (a member's own members are that member's gate's): what a member is follows from its content \u2014 an id, an L0 or a lock make it a project, anything else a part \u2014 and every contradiction is ONE finding, MEMBER_KIND_MISMATCH (error), naming the alias and the contradiction: an asserted `as` the content contradicts; a part whose configuration declares project fields (members, externals, rules, extensions, composition, targets) without the id that would make it a project (add the id, or move them to the parent); a non-contained part with no PartOf, or one naming another project (opened alone it could not say what it belongs to); a contained member declaring PartOf; a `use` on a member whose content is a part (its names are already this project's own). A part whose files could not be read \u2014 its directory absent, or its git commit not cached and not fetchable (offline, refused, unknown) \u2014 is PART_UNAVAILABLE (error): the project's own subsystems are missing, so its verdict cannot be a pass. The member findings name no spec: members live in configuration. It reads the project graph's nodes, their legacy mounts, the bound root's declared members and its problems.",
16530
16786
  codes: [
16531
- { code: "DEPRECATED_MOUNT_FORM", defaultSeverity: "notice", summary: "A member declared in a deprecated form \u2014 an L1 subsystem carrying projectPath, or (stage 8) the long-form `path` key \u2014 which doctor --fix rewrites to a `members` entry with one `source`." },
16787
+ { code: "DEPRECATED_MOUNT_FORM", defaultSeverity: "notice", summary: "A member declared in a deprecated form \u2014 an L1 subsystem carrying projectPath, or the long-form `path` key \u2014 which doctor --fix rewrites to a `members` entry with one `source`." },
16532
16788
  { code: "MEMBER_KIND_MISMATCH", defaultSeverity: "error", summary: "What a member's content makes it (an id, L0 or lock: a project; else a part) contradicts what is declared or written about it: an asserted `as`, project fields in a part, a missing or wrong PartOf, a `use` on a part." },
16533
16789
  { code: "PART_UNAVAILABLE", defaultSeverity: "error", summary: "A part's files could not be read (absent directory, unpinned or unfetchable git commit): the project's own subsystems are missing." }
16534
16790
  ],
@@ -16566,7 +16822,7 @@ var init_member_declarations = __esm({
16566
16822
  ctx.addIssue(
16567
16823
  "notice",
16568
16824
  "DEPRECATED_MOUNT_FORM",
16569
- `This project declares the member "${member.alias}" with the long-form \`path\` key, which stage 8 replaced by the one location key \`source\`. \`doctor --fix\` rewrites it to ${rest ? `\`source: ${written}\` in its long form` : `the shorthand \`${member.alias}: ${written}\``} \u2014 its meaning unchanged.`
16825
+ `This project declares the member "${member.alias}" with the long-form \`path\` key, which the one location key replaced: \`source\`. \`doctor --fix\` rewrites it to ${rest ? `\`source: ${written}\` in its long form` : `the shorthand \`${member.alias}: ${written}\``} \u2014 its meaning unchanged.`
16570
16826
  );
16571
16827
  }
16572
16828
  for (const problem of family.problems) {
@@ -16610,7 +16866,7 @@ var init_project_boundaries = __esm({
16610
16866
  projectBoundariesRule = {
16611
16867
  name: "project-boundaries",
16612
16868
  judges: "design",
16613
- description: "The one judge of every cross-project reference the bound project writes (decision 9 \u2014 every project is a crate of its own, reached only through its L0 exports and only as a dependency it declared). It reads each of the project's own references from the project graph \u2014 `::` forms and bare names bound through `use` imports alike \u2014 takes its resolution from resolveCrossProject, decided before any severity from the project's own files alone, and reports every outcome but resolved once, carrying the resolution: forbidden is EXTERNAL_UNDECLARED (the reference reaches a project the referrer declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers); missing is EXTERNAL_NOT_EXPORTED (the producer's table \u2014 a contained member's live one, or the external's pin \u2014 has no public name covering the referrer, or not the used method, or exports the type only inside a signature's closure); an ambiguous import is IMPORT_AMBIGUOUS (two imports supply one bare name: two explicit `use` names, or two `*` with no explicit one \u2014 name it `alias::name` or narrow a `use`); unavailable is EXTERNAL_CHECK_UNAVAILABLE (no pin, an unreadable pin, a member absent on disk, or a deprecated form naming no alias \u2014 never a pass). A bare name no import supplies is not this rule's: it is the unresolved-reference finding it always was, with rule_context.importHint appended. A trustedLinks entry naming a subsystem of another project (`alias::sub`) is TRUSTED_LINK_CROSSES_PROJECT: a sanctioned fast lane stays inside one project. Stage 4 makes the three stage-2 notices errors now that the owner's gate judges them exactly; a reference is never left to a parent's verdict.",
16869
+ description: "The one judge of every cross-project reference the bound project writes (every project is a crate of its own, reached only through its L0 exports and only as a dependency it declared). It reads each of the project's own references from the project graph \u2014 `::` forms and bare names bound through `use` imports alike \u2014 takes its resolution from resolveCrossProject, decided before any severity from the project's own files alone, and reports every outcome but resolved once, carrying the resolution: forbidden is EXTERNAL_UNDECLARED (the reference reaches a project the referrer declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers); missing is EXTERNAL_NOT_EXPORTED (the producer's table \u2014 a contained member's live one, or the external's pin \u2014 has no public name covering the referrer, or not the used method, or exports the type only inside a signature's closure); an ambiguous import is IMPORT_AMBIGUOUS (two imports supply one bare name: two explicit `use` names, or two `*` with no explicit one \u2014 name it `alias::name` or narrow a `use`); unavailable is EXTERNAL_CHECK_UNAVAILABLE (no pin, an unreadable pin, a member absent on disk, or a deprecated form naming no alias \u2014 never a pass). A bare name no import supplies is not this rule's: it is the unresolved-reference finding it always was, with rule_context.importHint appended. A trustedLinks entry naming a subsystem of another project (`alias::sub`) is TRUSTED_LINK_CROSSES_PROJECT: a sanctioned fast lane stays inside one project. An undeclared, unexported or ambiguous reference is an error, since the owner's gate judges it exactly; a reference is never left to a parent's verdict.",
16614
16870
  codes: [
16615
16871
  { code: "EXTERNAL_UNDECLARED", defaultSeverity: "error", summary: "A reference reaches another project the referring project declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers" },
16616
16872
  { code: "EXTERNAL_NOT_EXPORTED", defaultSeverity: "error", summary: "A reference reaches another project at anything but a public name, or a member of one, whose audience covers the referrer" },
@@ -19463,7 +19719,7 @@ var init_pack_reproducibility = __esm({
19463
19719
  ctx.addIssue(
19464
19720
  "warning",
19465
19721
  "PACK_SOURCE_UNFETCHABLE",
19466
- `Pack selection "${selection.name}" records no source and is not bundled, so a fresh machine or CI runner cannot obtain it \u2014 \`wairon pack sync\` has nothing to fetch. Record a URL (\`wairon pack use ${selection.name} --source <url>\`) or commit a copy (\`wairon pack bundle ${selection.name}\`).`
19722
+ `Pack selection "${selection.name}" records no source and is not bundled, so a fresh machine or CI runner cannot obtain it \u2014 \`wairon pack sync\` has nothing to fetch. Commit a copy the repository carries (\`wairon pack bundle ${selection.name}\`, or \`wairon pack bundle --all\` for every selection), or record a URL \`wairon pack sync\` fetches it from (\`wairon pack use ${selection.name} --source <url>\`). A selection \`wairon doctor --fix\` recorded from a pack installed from a local path has no URL to record: bundle it.`
19467
19723
  );
19468
19724
  }
19469
19725
  }
@@ -20565,6 +20821,18 @@ var init_invariant_backing = __esm({
20565
20821
  });
20566
20822
 
20567
20823
  // src/core/rules/wiring/invariant-references.ts
20824
+ function unknownInvariantMessage(where, ref, types, aliases) {
20825
+ const parts = splitInvariantRef(ref);
20826
+ const qualifier = parts ? parts.typeRef.lastIndexOf("::") : -1;
20827
+ if (parts && qualifier > 0) {
20828
+ const bare = `${parts.typeRef.slice(qualifier + 2)}.${parts.invariantId}`;
20829
+ if (resolveInvariantRef(bare, types, aliases)) {
20830
+ const prefix = parts.typeRef.slice(0, qualifier);
20831
+ return `${where} asserts invariant "${ref}", whose qualifier "${prefix}::" names no project this one reaches the type through \u2014 the invariant is declared here, so write "${bare}" (a project's own alias, written from inside it, is not a qualifier it reads as its own; \`wairon doctor --fix\` rewrites it in a migrated family).`;
20832
+ }
20833
+ }
20834
+ return `${where} asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`;
20835
+ }
20568
20836
  var invariantReferencesRule;
20569
20837
  var init_invariant_references = __esm({
20570
20838
  "src/core/rules/wiring/invariant-references.ts"() {
@@ -20573,7 +20841,7 @@ var init_invariant_references = __esm({
20573
20841
  invariantReferencesRule = {
20574
20842
  name: "invariant-references",
20575
20843
  judges: "design",
20576
- description: "Every invariant a narrative step asserts (step.assertsInvariants) must resolve to a declared entity invariant: the reference splits at its last dot into a type reference and an invariant id, and some entity matching that type reference must declare that id. A reference nothing declares is an error \u2014 the step claims a property that exists nowhere.",
20844
+ description: "Every invariant a narrative step asserts (step.assertsInvariants) must resolve to a declared entity invariant: the reference splits at its last dot into a type reference and an invariant id, and some entity matching that type reference must declare that id. A reference nothing declares is an error \u2014 the step claims a property that exists nowhere. The message says which: when the type reference carries a qualifier (`alias::type`) and the bare reference does resolve, the cause is the qualifier \u2014 typically the project's own alias written from inside it \u2014 and the message names it and the bare reference to write instead, never claiming that no entity declares the invariant.",
20577
20845
  codes: [
20578
20846
  { code: "UNKNOWN_INVARIANT_REF", defaultSeverity: "error", summary: "A narrative step asserts an invariant that no entity declares" }
20579
20847
  ],
@@ -20588,7 +20856,7 @@ var init_invariant_references = __esm({
20588
20856
  ctx.addIssue(
20589
20857
  "error",
20590
20858
  "UNKNOWN_INVARIANT_REF",
20591
- `Step ${step.stepNumber} of "${method2.name}" in implementation "${impl.id}" asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`,
20859
+ unknownInvariantMessage(`Step ${step.stepNumber} of "${method2.name}" in implementation "${impl.id}"`, ref, ctx.types, aliases),
20592
20860
  impl.id,
20593
20861
  isDraftCtx
20594
20862
  );
@@ -27296,8 +27564,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
27296
27564
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
27297
27565
  for (const step of steps) {
27298
27566
  for (const par of parallelArms) {
27299
- const sep18 = par.sepByStep.get(step.stepNumber);
27300
- if (sep18 !== void 0) lines.push(` and ${escapeLabel(sep18)}`);
27567
+ const sep19 = par.sepByStep.get(step.stepNumber);
27568
+ if (sep19 !== void 0) lines.push(` and ${escapeLabel(sep19)}`);
27301
27569
  }
27302
27570
  switch (step.type) {
27303
27571
  case "local":
@@ -28075,6 +28343,15 @@ function classifySource(world, sourceId, label7, sources) {
28075
28343
  detail: `"${label7}" re-exports from "${sourceId}", which is neither a subsystem of this project, a member nor a declared external the scan read \u2014 a project re-exports only from its subsystems, members and externals of its family`
28076
28344
  };
28077
28345
  }
28346
+ function duplicateDetail(entries, index) {
28347
+ const e = entries[index];
28348
+ if (e.from !== void 0 || e.component !== void 0 || e.interface !== void 0 || e.typeDef !== void 0) return null;
28349
+ const name = e.as ?? e.id;
28350
+ if (name === void 0) return null;
28351
+ const other = entries.findIndex((x, i) => i !== index && (x.as ?? x.id) === name);
28352
+ if (other === -1) return null;
28353
+ return `has a duplicate entry "${name}": entry #${index + 1} names no source and no item, while entry #${other + 1} declares the same id \u2014 delete the copy (entry #${index + 1}), or give it a source and an item under an id of its own`;
28354
+ }
28078
28355
  function bindOwnType(world, owner, e, problems) {
28079
28356
  const type = (world.types.get(e.typeDef) ?? []).find((t) => !t.subsystem);
28080
28357
  if (!type) {
@@ -28146,7 +28423,8 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
28146
28423
  const owner = sources.namespace ? sources.namespace : system.name;
28147
28424
  const problems = [];
28148
28425
  const candidates = [];
28149
- (system.publicInterfaces ?? []).forEach((e, index) => {
28426
+ const entries = system.publicInterfaces ?? [];
28427
+ entries.forEach((e, index) => {
28150
28428
  const label7 = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
28151
28429
  const source = classifySource(world, sourceOf(world, e), label7, sources);
28152
28430
  switch (source.kind) {
@@ -28172,7 +28450,7 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
28172
28450
  return;
28173
28451
  }
28174
28452
  default:
28175
- problems.push({ kind: "invalid", owner, publicName: label7, detail: source.detail });
28453
+ problems.push({ kind: "invalid", owner, publicName: label7, detail: duplicateDetail(entries, index) ?? source.detail });
28176
28454
  }
28177
28455
  });
28178
28456
  return settle(owner, "project", candidates, problems);
@@ -28769,6 +29047,16 @@ function qualifiedTypeNames(typeStr) {
28769
29047
  }
28770
29048
  return out;
28771
29049
  }
29050
+ function invariantTypeRef(ref) {
29051
+ const at = ref.lastIndexOf(".");
29052
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
29053
+ }
29054
+ function mapInvariantRef(ref, map) {
29055
+ const typeRef2 = invariantTypeRef(ref);
29056
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
29057
+ const next = map("type", typeRef2);
29058
+ return next === typeRef2 ? ref : `${next}${ref.slice(typeRef2.length)}`;
29059
+ }
28772
29060
  function rawReferences(kind, spec) {
28773
29061
  const out = [];
28774
29062
  const add2 = (position, value) => {
@@ -28791,6 +29079,7 @@ function rawReferences(kind, spec) {
28791
29079
  for (const step of m.narrative) {
28792
29080
  const source = step.auth?.from;
28793
29081
  if (typeof source === "string" && source.startsWith(COMPONENT_AUTH_SOURCE)) add2("auth", source.slice(COMPONENT_AUTH_SOURCE.length));
29082
+ for (const ref of step.assertsInvariants ?? []) add2("type", invariantTypeRef(ref));
28794
29083
  }
28795
29084
  }
28796
29085
  break;
@@ -28909,6 +29198,9 @@ function respellStoredReferences(kind, doc, map) {
28909
29198
  for (const m of list2(doc.methods)) {
28910
29199
  for (const step of list2(m.narrative)) {
28911
29200
  at(step, "targetComponent", "narrative");
29201
+ if (Array.isArray(step.assertsInvariants)) {
29202
+ step.assertsInvariants = step.assertsInvariants.map((ref) => typeof ref === "string" ? mapInvariantRef(ref, map) : ref);
29203
+ }
28912
29204
  const auth = step.auth;
28913
29205
  if (auth && typeof auth.from === "string" && auth.from.startsWith(COMPONENT_AUTH_SOURCE)) {
28914
29206
  auth.from = `${COMPONENT_AUTH_SOURCE}${map("auth", auth.from.slice(COMPONENT_AUTH_SOURCE.length))}`;
@@ -28968,7 +29260,8 @@ function mapRawReferences(kind, spec, map) {
28968
29260
  return call && next !== call.compId ? `${next}.${call.methodName}` : entry;
28969
29261
  })
28970
29262
  } : {},
28971
- narrative: m.narrative.map((step) => {
29263
+ narrative: m.narrative.map((authored) => {
29264
+ const step = authored.assertsInvariants ? { ...authored, assertsInvariants: authored.assertsInvariants.map((ref) => mapInvariantRef(ref, map)) } : authored;
28972
29265
  const source = step.auth?.from;
28973
29266
  if (typeof source !== "string" || !source.startsWith(COMPONENT_AUTH_SOURCE)) return step;
28974
29267
  const auth = step.auth;
@@ -29069,7 +29362,7 @@ function resolveFamilyTables(raws) {
29069
29362
  const byKey3 = new Map(raws.map((r) => [r.record.namespace, r]));
29070
29363
  const done = /* @__PURE__ */ new Set();
29071
29364
  const resolving = /* @__PURE__ */ new Set();
29072
- const resolve55 = (raw) => {
29365
+ const resolve56 = (raw) => {
29073
29366
  const key = raw.record.namespace;
29074
29367
  if (done.has(key)) return raw.record.exports;
29075
29368
  if (resolving.has(key)) return null;
@@ -29084,7 +29377,7 @@ function resolveFamilyTables(raws) {
29084
29377
  const producerKey = source !== void 0 ? raw.record.aliases.get(source) : void 0;
29085
29378
  if (producerKey === void 0 || producerTables.has(producerKey)) continue;
29086
29379
  const producer = byKey3.get(producerKey);
29087
- producerTables.set(producerKey, producer ? resolve55(producer) : null);
29380
+ producerTables.set(producerKey, producer ? resolve56(producer) : null);
29088
29381
  audienceOf.set(producerKey, "project");
29089
29382
  }
29090
29383
  raw.record.subsystemExports = subsystemTables;
@@ -29100,7 +29393,7 @@ function resolveFamilyTables(raws) {
29100
29393
  resolving.delete(key);
29101
29394
  }
29102
29395
  };
29103
- raws.forEach((raw) => resolve55(raw));
29396
+ raws.forEach((raw) => resolve56(raw));
29104
29397
  }
29105
29398
  function bindAuthored(raw, raws, readables, authored) {
29106
29399
  const key = raw.record.namespace;
@@ -31535,12 +31828,12 @@ var init_specs2 = __esm({
31535
31828
  return this.relativizeSpec("interface", stored);
31536
31829
  }
31537
31830
  prepareImplementationForWrite(spec, carry = true) {
31538
- const relative26 = this.relativizeSpec("implementation", spec, carry);
31831
+ const relative28 = this.relativizeSpec("implementation", spec, carry);
31539
31832
  const member = this.getSubprojectPrefix(spec.id);
31540
31833
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
31541
- if (memberRoot) return childRelativeFilePaths(relative26, this.rootDir, memberRoot);
31834
+ if (memberRoot) return childRelativeFilePaths(relative28, this.rootDir, memberRoot);
31542
31835
  const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
31543
- return partDir ? partRelativeFilePaths(relative26, this.rootDir, partDir) : relative26;
31836
+ return partDir ? partRelativeFilePaths(relative28, this.rootDir, partDir) : relative28;
31544
31837
  }
31545
31838
  /**
31546
31839
  * A type written in its stored form — every type position canonical (one
@@ -33697,7 +33990,7 @@ function resolveGit(external, config) {
33697
33990
  const declared = gitDeclarationOf(config, external);
33698
33991
  if (!declared) return { ...external, problem: "git producer unavailable: its declaration names no repository" };
33699
33992
  try {
33700
- const commit3 = external.role === "member" && declared.commit ? declared.commit : resolve7(declared.url, declared.ref);
33993
+ const commit3 = external.role === "member" && declared.commit ? declared.commit : resolve3(declared.url, declared.ref);
33701
33994
  const directory = fetch2(declared.url, commit3, declared.dir);
33702
33995
  return { ...external, directory: path23.resolve(directory), commit: commit3, problem: void 0 };
33703
33996
  } catch (e) {
@@ -34148,7 +34441,7 @@ function scaffoldPart(alias, memberDir, contained) {
34148
34441
  }
34149
34442
  function createGitMember(alias, source, description, as) {
34150
34443
  const url = source.git;
34151
- const commit3 = source.commit ?? resolve7(url, source.ref);
34444
+ const commit3 = source.commit ?? resolve3(url, source.ref);
34152
34445
  const dir = fetch2(url, commit3, source.dir);
34153
34446
  const kind = holdsProjectContent(dir) ? "project" : "part";
34154
34447
  if (as !== void 0 && as !== kind) {
@@ -35090,7 +35383,7 @@ function advanceMember(alias, ref, commit3, dryRun) {
35090
35383
  const url = member.source.git;
35091
35384
  const from = member.source.commit ?? "";
35092
35385
  const followed = commit3 === void 0 ? ref ?? member.source.ref : void 0;
35093
- const to = commit3 ?? resolve7(url, followed);
35386
+ const to = commit3 ?? resolve3(url, followed);
35094
35387
  const after = specContents(fetch2(url, to, member.source.dir));
35095
35388
  const before = specContents(from !== "" ? fetch2(url, from, member.source.dir) : null);
35096
35389
  const added = [...after.keys()].filter((f) => !before.has(f)).sort();
@@ -36699,17 +36992,17 @@ function memberHop(agentId) {
36699
36992
  const alias = agentId.slice(0, at);
36700
36993
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
36701
36994
  if (!member) return null;
36702
- const relative26 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
36703
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative26 };
36995
+ const relative28 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
36996
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative28 };
36704
36997
  }
36705
- function throughMount(brief, alias, relative26) {
36706
- const under = (p) => path29.posix.join(relative26, p);
36998
+ function throughMount(brief, alias, relative28) {
36999
+ const under = (p) => path29.posix.join(relative28, p);
36707
37000
  return {
36708
37001
  ...brief,
36709
37002
  agentId: `${alias}::${brief.agentId}`,
36710
37003
  ownedPaths: brief.ownedPaths.map(under),
36711
37004
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
36712
- root: brief.root ? under(brief.root) : relative26
37005
+ root: brief.root ? under(brief.root) : relative28
36713
37006
  };
36714
37007
  }
36715
37008
  function loadRegistry() {
@@ -40100,8 +40393,8 @@ var require_dist = __commonJS({
40100
40393
  walkPackDir(root, path93.join(current2, entry.name), out);
40101
40394
  } else if (entry.isFile()) {
40102
40395
  const absolute = path93.join(current2, entry.name);
40103
- const relative26 = path93.relative(root, absolute).split(path93.sep).join("/");
40104
- out.push({ path: relative26, contents: fs69.readFileSync(absolute) });
40396
+ const relative28 = path93.relative(root, absolute).split(path93.sep).join("/");
40397
+ out.push({ path: relative28, contents: fs69.readFileSync(absolute) });
40105
40398
  }
40106
40399
  }
40107
40400
  }
@@ -40301,8 +40594,8 @@ var require_dist = __commonJS({
40301
40594
  return normalized === "." ? ".wai/" : `${normalized}/.wai/`;
40302
40595
  }
40303
40596
  function normalizeRootPath(relativePath) {
40304
- const posix6 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
40305
- return posix6 === "" || posix6 === "." ? "." : posix6;
40597
+ const posix8 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
40598
+ return posix8 === "" || posix8 === "." ? "." : posix8;
40306
40599
  }
40307
40600
  function slugify2(projectName) {
40308
40601
  const slug2 = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -40408,19 +40701,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
40408
40701
  const backupDir = path34.join(root, BACKUP_DIR, timestampSlug());
40409
40702
  let backedUp = false;
40410
40703
  for (const rootRel of roots) {
40411
- const relative26 = rootRel === "." ? "" : rootRel;
40412
- const stagedSpecDir = path34.join(stagingDir, relative26, ".wai");
40704
+ const relative28 = rootRel === "." ? "" : rootRel;
40705
+ const stagedSpecDir = path34.join(stagingDir, relative28, ".wai");
40413
40706
  if (!fs20.existsSync(stagedSpecDir)) continue;
40414
- const liveSpecDir = liveSpecDirFor(path34.join(root, relative26));
40707
+ const liveSpecDir = liveSpecDirFor(path34.join(root, relative28));
40415
40708
  if (backupExisting && liveSpecDir && fs20.existsSync(liveSpecDir)) {
40416
- const backupTarget = path34.join(backupDir, relative26, path34.basename(liveSpecDir));
40709
+ const backupTarget = path34.join(backupDir, relative28, path34.basename(liveSpecDir));
40417
40710
  fs20.mkdirSync(path34.dirname(backupTarget), { recursive: true });
40418
40711
  fs20.renameSync(liveSpecDir, backupTarget);
40419
40712
  backedUp = true;
40420
40713
  } else if (liveSpecDir && fs20.existsSync(liveSpecDir)) {
40421
40714
  fs20.rmSync(liveSpecDir, { recursive: true, force: true });
40422
40715
  }
40423
- const target = path34.join(root, relative26, ".wai");
40716
+ const target = path34.join(root, relative28, ".wai");
40424
40717
  fs20.mkdirSync(path34.dirname(target), { recursive: true });
40425
40718
  if (fs20.existsSync(target)) fs20.rmSync(target, { recursive: true, force: true });
40426
40719
  fs20.renameSync(stagedSpecDir, target);
@@ -40735,7 +41028,7 @@ function repairTypeSpellings(apply4) {
40735
41028
  const key = `${kind}:${specId}`;
40736
41029
  let repair = repairs.get(key);
40737
41030
  if (!repair) {
40738
- repair = { specId, kind, rewritten: [], proposals: [], authorNeeded: [] };
41031
+ repair = { specId, kind, rewritten: [], proposals: [], enumProposals: [], authorNeeded: [] };
40739
41032
  repairs.set(key, repair);
40740
41033
  }
40741
41034
  return repair;
@@ -40750,7 +41043,9 @@ function repairTypeSpellings(apply4) {
40750
41043
  if (problem.specId === void 0 || problem.kind === void 0 || !own(problem.specId)) continue;
40751
41044
  const repair = repairOf(problem.specId, problem.kind);
40752
41045
  const proposal = typeProblemIntProposal(problem);
41046
+ const enumProposal = proposal ? null : typeProblemEnumProposal(problem);
40753
41047
  if (proposal) repair.proposals.push(proposal);
41048
+ else if (enumProposal) repair.enumProposals.push(enumProposal);
40754
41049
  else repair.authorNeeded.push(problem);
40755
41050
  }
40756
41051
  const planned = [...repairs.values()];
@@ -40960,7 +41255,7 @@ var init_approval = __esm({
40960
41255
  init_lockfile();
40961
41256
  init_lock();
40962
41257
  init_project_family();
40963
- GATE_UPGRADE_NOTE = "the gate identity gained inputs in stage 5 (members' composition subjects, `composition`; code conformance moved beside the claim)";
41258
+ GATE_UPGRADE_NOTE = "the gate identity gained inputs in this release (members' composition subjects, `composition`; code conformance moved beside the claim)";
40964
41259
  }
40965
41260
  });
40966
41261
 
@@ -41139,16 +41434,16 @@ var init_ai_guide = __esm({
41139
41434
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
41140
41435
 
41141
41436
  ### In SDD Projects:
41142
- - **Source of Truth**: All architecture lives in the spec tree under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 L5 Narrative). Do not edit generated agent config files under \`.claude/agents/\` (rebuilt via \`wairon generate\`).
41437
+ - **Source of Truth**: All architecture lives in the spec tree under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 L5 Narrative). Agent files under \`.claude/agents/\` are an opt-in generated view (\`rules.materializeAgentFiles\`) \u2014 never edit them; agents are served as live briefs.
41143
41438
  - **Validation**: Conformance checks (stereotype rules, cycle checks, reference integrity) are run via the \`sdd_validate_tree\` MCP tool.
41144
41439
  - **Operating Rules**:
41145
41440
  1. **Skills**: Use \`sdd-architect\` to design (and \`sdd-implement\`, \`sdd-narrative\`, \`sdd-auditor\`). Refer to project's local guide file for detailed constraints.
41146
41441
  2. **MCP Tools Only**: Author/validate specs *only* via \`sdd_*\` tools (e.g. \`sdd_initialize_system\`, \`sdd_validate_tree\`).
41147
41442
  3. **No CLI Exec**: Do not run the \`wairon\` CLI (human tool). Use MCP tools \`sdd_validate_tree\` and \`sdd_get_status\` instead.
41148
41443
  4. **Delegation**: Delegate implementation via the \`sdd-delegate\` skill \u2014 live agent briefs (\`sdd_get_agent_brief\` MCP tool / \`wairon-agent://\` resource) are composed per call and always current; no session restart. Generated agent files are an optional materialized view of the same topology. User-owned per-agent guidance may live in \`.wai/agents/<agent-id>.md\` (folded into every brief; scaffold via \`wairon agent customize <id>\`).
41149
- 5. **Design First**: Complete spec and pass \`sdd_validate_tree\` before writing code.
41444
+ 5. **Design First, then approval**: Complete the spec and pass \`sdd_validate_tree\`, then ask the human to approve it with \`wairon lock\` before writing code. Approval is the lock record (\`.wai/lock.json\`), not a spec's \`status\` \u2014 the lock does not rewrite statuses; \`sdd_get_status\` reports the approval state.
41150
41445
  6. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
41151
- 7. **Members & References**: A project may contain other wairon projects as **members**, declared in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`); each member has its own spec tree, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.`;
41446
+ 7. **Members & References**: A project may declare **members** in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`). A **part** (the default) stores some of this project's subsystems in another folder or repository: local ids, this project's lock. A **project** member is an independent boundary with its own spec tree and lock, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.`;
41152
41447
  LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
41153
41448
 
41154
41449
  This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 Narrative); agent topology and code are derived from it.
@@ -41165,7 +41460,8 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
41165
41460
  - **\`alias::name\`**: An id without \`::\` is local to the project that writes it. Anything another project provides is referenced as \`alias::name\` \u2014 the alias is one of your members or declared \`externals\`, the name a public name in that project's L0 export table. A reference to something it does not export is reported (\`EXTERNAL_NOT_EXPORTED\`).
41166
41461
  - **Deprecated forms** (they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them): a leading \`::\` (\`::shared::error-type\`), \`super::\` (\`super::sibling_comp\`), member paths (\`billing::invoice::invoice_portal\`), and an L1 subsystem carrying \`projectPath\` (\`DEPRECATED_MOUNT_FORM\`).
41167
41462
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
41168
- - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to confirm and freeze them."* No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
41463
+ - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
41464
+ - **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`.
41169
41465
  - **To implement code**: Delegate via the \`sdd-delegate\` skill: fetch the component's live brief with the \`sdd_get_agent_brief\` MCP tool (or the \`wairon-agent://\` resource) and spawn a subagent from it. Briefs are composed per call from the current spec tree, so they are always current \u2014 never wait for a restart. Implementations must match L3 interfaces and L5 narratives exactly. Generated agent files under \`.claude/agents/\` are an optional materialized view of the same topology \u2014 the live briefs are canonical.
41170
41466
 
41171
41467
  ### Rules (enforced by \`sdd_validate_tree\`)
@@ -41545,7 +41841,7 @@ function getStatusReport(options = {}, decor) {
41545
41841
  }
41546
41842
  const own = options.approvals?.find((a) => a.key === "");
41547
41843
  const hasMembers = (options.approvals ?? []).some((a) => a.key !== "" && a.as !== "part");
41548
- if (own && (own.state !== "never" || hasMembers)) output += `${mark.layer("system", "Approval:")} this project is ${own.state}${own.upgraded ? " (locked under the pre-stage-5 gate identity \u2014 re-lock once)" : ""}
41844
+ if (own && (own.state !== "never" || hasMembers)) output += `${mark.layer("system", "Approval:")} this project is ${own.state}${own.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}
41549
41845
  `;
41550
41846
  return { text: output, failed: false };
41551
41847
  }
@@ -43211,7 +43507,7 @@ var init_position_migration = __esm({
43211
43507
  auth: ["implementation"],
43212
43508
  subsystem: ["component", "type"],
43213
43509
  group: ["type"],
43214
- type: ["interface", "type"]
43510
+ type: ["interface", "type", "implementation"]
43215
43511
  };
43216
43512
  BOUND = /* @__PURE__ */ new Set(["publicInterfaces", "lifecycle", "subsystem", "owns", "dependsOn", "dispatch", "mounts", "contract", "narrative", "group"]);
43217
43513
  compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -44172,7 +44468,7 @@ function attachEdits(plan7, family, bound2, dir, alias, bindingConfig) {
44172
44468
  edit(plan7, key, "id", `id: ${id} (declared${approved !== void 0 ? ", the id its lock approved" : ""})`, { root: dir, call: "setId", args: [id] });
44173
44469
  }
44174
44470
  if (!attached) plan7.notes.push(`${dir} has no project.yaml of its own \u2014 \`wairon doctor --fix\` writes one declaring "${alias}" as its id`);
44175
- const relPath = posix3(path45.relative(familyNode(family, bound2).directory, dir));
44471
+ const relPath = posix5(path45.relative(familyNode(family, bound2).directory, dir));
44176
44472
  const declaration2 = { path: relPath, ...plan7.request.description !== void 0 ? { description: plan7.request.description } : {} };
44177
44473
  edit(plan7, bound2, "member", `members: ${alias} \u2192 ${relPath} (an existing project, kept as it is)`, { root: familyNode(family, bound2).directory, call: "declareMember", args: [alias, declaration2] });
44178
44474
  for (const requirement of requiredPolicies(bindingConfig)) {
@@ -44219,8 +44515,8 @@ function planDetach(family, bound2, request) {
44219
44515
  if (plan7.request.widen) planWidening(plan7, member, narrow);
44220
44516
  if (plan7.refusals.length > 0) return plan7;
44221
44517
  const boundDir = familyNode(family, bound2).directory;
44222
- const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix3(path45.relative(boundDir, member.directory)));
44223
- const declared = { ...held, path: memberLocationOf(held) ?? posix3(path45.relative(boundDir, member.directory)) };
44518
+ const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix5(path45.relative(boundDir, member.directory)));
44519
+ const declared = { ...held, path: memberLocationOf(held) ?? posix5(path45.relative(boundDir, member.directory)) };
44224
44520
  detachEdits(plan7, family, bound2, member, alias, declared, consumers, outward);
44225
44521
  return plan7;
44226
44522
  }
@@ -44260,7 +44556,7 @@ function planWidening(plan7, member, narrow) {
44260
44556
  function detachEdits(plan7, family, bound2, member, alias, declared, consumers, outward) {
44261
44557
  const boundDir = familyNode(family, bound2).directory;
44262
44558
  const relocation = plan7.request.relocation;
44263
- const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix3(path45.relative(from, producer.directory)) };
44559
+ const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix5(path45.relative(from, producer.directory)) };
44264
44560
  const where = (source) => source.hosted !== void 0 ? `hosted record ${source.hosted}` : source.path;
44265
44561
  edit(plan7, bound2, "member", `members: ${alias} removed (${declared.path} ${relocation ? `moves to ${relocation.to}` : "stays as it is"})`, { root: boundDir, call: "removeMember", args: [alias] });
44266
44562
  const boundSource = relocation ? sourceTo(boundDir, member) : { path: declared.path };
@@ -44334,7 +44630,7 @@ function adoptEdits(plan7, family, bound2, alias, external, dir, at = dir) {
44334
44630
  edit(plan7, bound2, "pin", `pin ${alias} removed (the family supplies it now)`, { root: boundDir, call: "unpin", args: [alias] });
44335
44631
  edit(plan7, bound2, "external", `externals: ${alias} removed`, { root: boundDir, call: "removeExternal", args: [alias] });
44336
44632
  const declaration2 = {
44337
- path: posix3(path45.relative(boundDir, at)),
44633
+ path: posix5(path45.relative(boundDir, at)),
44338
44634
  ...external.description !== void 0 ? { description: external.description } : {},
44339
44635
  ...external.use && external.use.length > 0 ? { use: external.use } : {}
44340
44636
  };
@@ -44407,7 +44703,7 @@ function writeOne(rehearsal, w) {
44407
44703
  }
44408
44704
  });
44409
44705
  }
44410
- var fs26, path45, posix3, refuse, edit, label4;
44706
+ var fs26, path45, posix5, refuse, edit, label4;
44411
44707
  var init_membership = __esm({
44412
44708
  "src/migrations/membership.ts"() {
44413
44709
  "use strict";
@@ -44422,7 +44718,7 @@ var init_membership = __esm({
44422
44718
  init_project_family();
44423
44719
  init_specs();
44424
44720
  init_types2();
44425
- posix3 = (p) => p.split(path45.sep).join("/");
44721
+ posix5 = (p) => p.split(path45.sep).join("/");
44426
44722
  refuse = (plan7, code, project2, detail) => {
44427
44723
  plan7.refusals.push({ code, project: project2, detail });
44428
44724
  };
@@ -44670,7 +44966,7 @@ var init_identity = __esm({
44670
44966
  auth: ["implementation"],
44671
44967
  subsystem: ["component", "type"],
44672
44968
  group: ["type"],
44673
- type: ["interface", "type"]
44969
+ type: ["interface", "type", "implementation"]
44674
44970
  };
44675
44971
  PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin"];
44676
44972
  }
@@ -44868,7 +45164,7 @@ function respellConsumer(plan7, family, c, own) {
44868
45164
  function repointConsumer(plan7, c, boundNode2) {
44869
45165
  const declared = configAt5(c.node.directory)?.externals?.[c.alias];
44870
45166
  const project2 = boundNode2.id !== void 0 && boundNode2.id !== c.alias ? boundNode2.id : null;
44871
- const source = declared?.source ? { path: posix4(path47.relative(c.node.directory, boundNode2.directory)) } : null;
45167
+ const source = declared?.source ? { path: posix6(path47.relative(c.node.directory, boundNode2.directory)) } : null;
44872
45168
  edit3(plan7, c.node.namespace, "external", `externals: ${c.alias} names ${boundNode2.id ?? "the top project"}${source ? ` at ${source.path}` : ""}`, { root: c.node.directory, call: "repointExternal", args: [c.alias, project2, source] });
44873
45169
  plan7.notes.push(`${label6(c.node.namespace)}'s pin of "${c.alias}" is taken again against ${boundNode2.id ?? "the top project"}: the producer changed, so its lock reads drifted until it is re-locked.`);
44874
45170
  return c;
@@ -44904,8 +45200,8 @@ function planExternalize(family, bound2, request) {
44904
45200
  plan7,
44905
45201
  bound2,
44906
45202
  "move",
44907
- `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix4(path47.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
44908
- { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path47.relative(node.directory, dir))] }
45203
+ `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix6(path47.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
45204
+ { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path47.relative(node.directory, dir))] }
44909
45205
  );
44910
45206
  plan7.notes.push("Source code is not moved: move the subsystem's code yourself if it should live beside its specs.");
44911
45207
  return plan7;
@@ -44914,7 +45210,7 @@ function planExternalize(family, bound2, request) {
44914
45210
  crossingsBack(plan7, family, bound2, subKey);
44915
45211
  checkSiblings(plan7, family, bound2, id, subKey, table);
44916
45212
  if (plan7.refusals.length > 0) return plan7;
44917
- edit3(plan7, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix4(path47.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path47.relative(node.directory, dir)), "project"] });
45213
+ edit3(plan7, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix6(path47.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path47.relative(node.directory, dir)), "project"] });
44918
45214
  edit3(plan7, id, "pin", `pin the new member's externals (its external for ${node.id ?? "this project"}, when its specs reference it)`, { root: dir, call: "pinExternals", args: [[]] });
44919
45215
  plan7.notes.push("Source code is not moved: move the subsystem's code into the member yourself.");
44920
45216
  return plan7;
@@ -45159,7 +45455,7 @@ function writeOne3(rehearsal, w) {
45159
45455
  }
45160
45456
  });
45161
45457
  }
45162
- var path47, posix4, label6, refuse3, edit3, KINDS_AT3;
45458
+ var path47, posix6, label6, refuse3, edit3, KINDS_AT3;
45163
45459
  var init_boundary = __esm({
45164
45460
  "src/migrations/boundary.ts"() {
45165
45461
  "use strict";
@@ -45171,7 +45467,7 @@ var init_boundary = __esm({
45171
45467
  init_project();
45172
45468
  init_project_family();
45173
45469
  init_types2();
45174
- posix4 = (p) => p.split(path47.sep).join("/");
45470
+ posix6 = (p) => p.split(path47.sep).join("/");
45175
45471
  label6 = (key) => key === "" ? "the top project" : `"${key}"`;
45176
45472
  refuse3 = (plan7, code, project2, detail) => {
45177
45473
  plan7.refusals.push({ code, project: project2, detail });
@@ -45193,7 +45489,7 @@ var init_boundary = __esm({
45193
45489
  auth: ["implementation"],
45194
45490
  subsystem: ["component", "type"],
45195
45491
  group: ["type"],
45196
- type: ["interface", "type"]
45492
+ type: ["interface", "type", "implementation"]
45197
45493
  };
45198
45494
  }
45199
45495
  });
@@ -46222,6 +46518,18 @@ function renderChangeReport(report4) {
46222
46518
  lines.push(...renderTestsToRevisit(report4.testsToRevisit));
46223
46519
  return lines.join("\n");
46224
46520
  }
46521
+ function specKindsHolding(id) {
46522
+ const system = loadSystemSpec();
46523
+ const held = {
46524
+ system: !!system && (id === "system" || id === system.name),
46525
+ subsystem: !!loadSubsystemSpec(id),
46526
+ component: !!loadComponentSpec(id),
46527
+ interface: !!loadInterfaceSpec(id),
46528
+ implementation: !!loadImplementationSpec(id),
46529
+ type: !!loadTypeSpec(id)
46530
+ };
46531
+ return SPEC_KINDS.filter((k) => held[k]);
46532
+ }
46225
46533
  function renderTestsToRevisit(entries) {
46226
46534
  if (!entries.length) return [];
46227
46535
  const lines = ["", "TESTS TO REVISIT:"];
@@ -46806,7 +47114,7 @@ ${renderChangeReport(report4)}`,
46806
47114
  server,
46807
47115
  "sdd_add_member",
46808
47116
  {
46809
- description: "Create a member of the bound project at `source`, in the one members grammar: `services/scheduler` (contained), `../admin` (a sibling checkout), `git@host:acme/payments.git` (a git repository, pinned at its default branch head unless `#<commit>` is given). By default a PART (stage 8): a piece of this project stored in another folder or repository, whose subsystems are this project's own (local ids, the ordinary subsystem rules, this project's lock); it gets only a specs folder, plus a configuration naming this project when it is not contained. With `as: project` an independent PROJECT instead: its project content is created \u2014 a project.yaml declaring the alias as its id, its L0, and the bound project's required packs (composition.requirePolicies) once \u2014 and it is reached only as `alias::name` through its L0 exports. What a member is follows from that content. A git member is not scaffolded; the repository's content decides what it is. Boundaries are earned: create a part unless the piece needs its own team, release, approval or public surface; sdd_promote_member makes one later. Idempotent \u2014 the same call again writes nothing; a different member under the alias is refused.",
47117
+ description: "Create a member of the bound project at `source`, in the one members grammar: `services/scheduler` (contained), `../admin` (a sibling checkout), `git@host:acme/payments.git` (a git repository, pinned at its default branch head unless `#<commit>` is given). By default a PART: a piece of this project stored in another folder or repository, whose subsystems are this project's own (local ids, the ordinary subsystem rules, this project's lock); it gets only a specs folder, plus a configuration naming this project when it is not contained. With `as: project` an independent PROJECT instead: its project content is created \u2014 a project.yaml declaring the alias as its id, its L0, and the bound project's required packs (composition.requirePolicies) once \u2014 and it is reached only as `alias::name` through its L0 exports. What a member is follows from that content. A git member is not scaffolded; the repository's content decides what it is. Boundaries are earned: create a part unless the piece needs its own team, release, approval or public surface; sdd_promote_member makes one later. Idempotent \u2014 the same call again writes nothing; a different member under the alias is refused.",
46810
47118
  inputSchema: {
46811
47119
  alias: import_zod12.z.string().describe("The member's alias ([a-z0-9-_]+); a project's new id"),
46812
47120
  source: import_zod12.z.string().describe("A contained path, a `../` sibling path, or a git URL (optionally `#<commit>`)"),
@@ -46876,7 +47184,7 @@ ${renderChangeReport(report4)}`,
46876
47184
  server,
46877
47185
  "sdd_externalize_subsystem",
46878
47186
  {
46879
- description: "Move an internal subsystem out to the path. By default (stage 8) into a PART \u2014 a storage move: the subsystem's specs move to that directory (a new part, or the existing part there), and nothing else changes: no reference, export, external or lock entry, the same verdict. With `as: project`, into a member PROJECT instead, family-wide and all-or-nothing \u2014 the move followed by a promote: what crosses the new boundary exported and imported, references respelled `alias::name`, the new member's external for this project pinned, and every other family project's references checked to still resolve (a reference that would need a component made public refuses the plan). Source code is not moved. dryRun answers the plan and writes nothing; run it first and show the plan. Never locks: the answer names the projects to re-lock.",
47187
+ description: "Move an internal subsystem out to the path. By default into a PART \u2014 a storage move: the subsystem's specs move to that directory (a new part, or the existing part there), and nothing else changes: no reference, export, external or lock entry, the same verdict. With `as: project`, into a member PROJECT instead, family-wide and all-or-nothing \u2014 the move followed by a promote: what crosses the new boundary exported and imported, references respelled `alias::name`, the new member's external for this project pinned, and every other family project's references checked to still resolve (a reference that would need a component made public refuses the plan). Source code is not moved. dryRun answers the plan and writes nothing; run it first and show the plan. Never locks: the answer names the projects to re-lock.",
46880
47188
  inputSchema: {
46881
47189
  subsystem: import_zod12.z.string().describe("The subsystem to move out"),
46882
47190
  path: import_zod12.z.string().describe("The part's root, relative to the bound project (a new directory, or an existing part's)"),
@@ -46890,7 +47198,7 @@ ${renderChangeReport(report4)}`,
46890
47198
  server,
46891
47199
  "sdd_promote_member",
46892
47200
  {
46893
- description: "Promote a part of the bound project to an independent project in place (stage 8) \u2014 its files stay where they are; it gets an id, an L0 exporting exactly what the rest of the project uses of it, its references across the new boundary respelled `alias::name`, the parent declared as its external, and pins on both sides. Refused (each reason listed) for a part fetched from git, a trusted link that would cross the new boundary, a used component its subsystem does not publish, or an id already taken. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks: the answer names the project to re-lock and the new one to lock.",
47201
+ description: "Promote a part of the bound project to an independent project in place \u2014 its files stay where they are; it gets an id, an L0 exporting exactly what the rest of the project uses of it, its references across the new boundary respelled `alias::name`, the parent declared as its external, and pins on both sides. Refused (each reason listed) for a part fetched from git, a trusted link that would cross the new boundary, a used component its subsystem does not publish, or an id already taken. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks: the answer names the project to re-lock and the new one to lock.",
46894
47202
  inputSchema: {
46895
47203
  alias: import_zod12.z.string().describe("The part's alias"),
46896
47204
  id: import_zod12.z.string().optional().describe("The new project's id (default: the alias)"),
@@ -46903,7 +47211,7 @@ ${renderChangeReport(report4)}`,
46903
47211
  server,
46904
47212
  "sdd_demote_member",
46905
47213
  {
46906
- description: "Demote a project member of the bound project to a part in place (stage 8) \u2014 sdd_promote_member's inverse: its files stay where they are; its own metadata goes to the homes the destination names (as sdd_internalize_member sends it), its L0, lock and pins end, its references across the old boundary become local ids, and its configuration is reduced to naming its parent. Refused while another family project consumes it (a part has no exports \u2014 each consumer listed), for a member fetched from git, or for internalize's reasons. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks.",
47214
+ description: "Demote a project member of the bound project to a part in place \u2014 sdd_promote_member's inverse: its files stay where they are; its own metadata goes to the homes the destination names (as sdd_internalize_member sends it), its L0, lock and pins end, its references across the old boundary become local ids, and its configuration is reduced to naming its parent. Refused while another family project consumes it (a part has no exports \u2014 each consumer listed), for a member fetched from git, or for internalize's reasons. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks.",
46907
47215
  inputSchema: {
46908
47216
  alias: import_zod12.z.string().describe("The project member's alias"),
46909
47217
  destination: import_zod12.z.object({
@@ -47246,7 +47554,7 @@ ${renderChangeReport(report4)}`,
47246
47554
  interface: import_zod12.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
47247
47555
  endpoints: import_zod12.z.array(import_zod12.z.object({
47248
47556
  method: import_zod12.z.string().describe("The NAME of the interface method to bind (not the HTTP verb)"),
47249
- transport: import_zod12.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must match the Portal's portalType"),
47557
+ transport: import_zod12.z.enum(["HTTP", "HTTP_API", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must be the one the Portal's portalType implies. Either vocabulary is accepted: the endpoint's (HTTP) or the portalType's (HTTP_API), stored as HTTP; any other transport is refused, naming the one the portalType implies"),
47250
47558
  httpMethod: import_zod12.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
47251
47559
  path: import_zod12.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
47252
47560
  service: import_zod12.z.string().optional().describe("gRPC: service name"),
@@ -47269,8 +47577,10 @@ ${renderChangeReport(report4)}`,
47269
47577
  try {
47270
47578
  const intf = loadInterfaceSpec(interfaceId);
47271
47579
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
47272
- const buildRaw = (e) => {
47273
- switch (e.transport) {
47580
+ const owner = intf.component ? loadComponentSpec(intf.component) : null;
47581
+ const implied = owner?.componentType === "Portal" && owner.portalType ? PORTAL_TYPE_TRANSPORT[owner.portalType] : void 0;
47582
+ const buildRaw = (e, transport) => {
47583
+ switch (transport) {
47274
47584
  case "HTTP":
47275
47585
  return { transport: "HTTP", method: e.httpMethod, path: e.path };
47276
47586
  case "gRPC":
@@ -47294,13 +47604,19 @@ ${renderChangeReport(report4)}`,
47294
47604
  for (const e of endpoints) {
47295
47605
  const m = intf.methods.find((x) => x.name === e.method);
47296
47606
  if (!m) return errText(`Method "${e.method}" not found on interface "${interfaceId}". Define it via sdd_define_interface first.`);
47297
- const parsed = EndpointSchema.safeParse(buildRaw(e));
47607
+ const transport = e.transport === "HTTP_API" ? "HTTP" : e.transport;
47608
+ if (implied && transport !== implied) {
47609
+ return errText(
47610
+ `Method "${e.method}": transport "${e.transport}" does not match ${owner.id}'s portalType ${owner.portalType}, which implies transport "${implied}". Bind it as "${implied}", or change the Portal's portalType.`
47611
+ );
47612
+ }
47613
+ const parsed = EndpointSchema.safeParse(buildRaw(e, transport));
47298
47614
  if (!parsed.success) {
47299
47615
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
47300
- return errText(`Invalid ${e.transport} endpoint for method "${e.method}": ${detail}`);
47616
+ return errText(`Invalid ${transport} endpoint for method "${e.method}": ${detail}`);
47301
47617
  }
47302
47618
  methods.push({ name: e.method, endpoint: parsed.data });
47303
- bound2.push(`${e.method}\u2192${e.transport}`);
47619
+ bound2.push(`${e.method}\u2192${transport}`);
47304
47620
  }
47305
47621
  const report4 = updateSpecGated("interface", interfaceId, { methods });
47306
47622
  return structured(
@@ -47561,14 +47877,24 @@ ${renderChangeReport(report4)}`,
47561
47877
  {
47562
47878
  description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
47563
47879
  inputSchema: {
47564
- kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
47880
+ kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).optional().describe("The kind of specification. Omit it to infer the kind from the id: the read is refused, naming the candidate kinds, when the id names specs at more than one level (an interface and an implementation sharing an id, say), or at none"),
47565
47881
  id: import_zod12.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
47566
47882
  methods: import_zod12.z.array(import_zod12.z.string().min(1)).optional().describe("Return only these methods by name; every other field of the spec comes back unchanged. Only an interface, an implementation or a type declares methods \u2014 asking for one elsewhere is refused, as is a name the spec does not declare (the answer names the ones it does). Omit it for the whole spec.")
47567
47883
  },
47568
47884
  outputSchema: getSpecOutput
47569
47885
  },
47570
- ({ kind, id, methods }) => {
47886
+ ({ kind: askedKind, id, methods }) => {
47571
47887
  try {
47888
+ let kind;
47889
+ if (askedKind) {
47890
+ kind = askedKind;
47891
+ } else {
47892
+ const holders = specKindsHolding(id);
47893
+ if (holders.length !== 1) {
47894
+ return errText(holders.length === 0 ? `No spec of any kind has the ID "${id}".` : `The ID "${id}" names specs of more than one kind (${holders.join(", ")}). Pass "kind" to choose one.`);
47895
+ }
47896
+ kind = holders[0];
47897
+ }
47572
47898
  let result = null;
47573
47899
  let partial2 = null;
47574
47900
  switch (kind) {
@@ -47927,7 +48253,7 @@ async function startMcpServer() {
47927
48253
  } catch {
47928
48254
  }
47929
48255
  }
47930
- var import_mcp, import_stdio, import_zod12, import_types12, fs29, path50, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalStatusesOutput, SPEC_KINDS, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
48256
+ var import_mcp, import_stdio, import_zod12, import_types12, fs29, path50, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalStatusesOutput, SPEC_KINDS, PORTAL_TYPE_TRANSPORT, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
47931
48257
  var init_server = __esm({
47932
48258
  "src/mcp/server.ts"() {
47933
48259
  "use strict";
@@ -47956,7 +48282,7 @@ var init_server = __esm({
47956
48282
  init_project();
47957
48283
  init_validation_options();
47958
48284
  init_migrations3();
47959
- TYPE_REF_GRAMMAR = (what) => `${what}, in the neutral type grammar: a primitive (string, int, float, bool, bytes, date, datetime, duration, any; void only as a whole returns), a defined type id ("billing.Invoice", "alias::name"), list<T>, set<T>, map<K, V> (K is string, int or an enum), T? (T or no value), a union of NAMED types ("Invoice | Receipt"), and on a returns only, async T ("async void"). Every named type must resolve. TypeScript spellings are accepted and respelled \u2014 "Invoice[]" \u2192 list<Invoice>, "Invoice | null" \u2192 Invoice?, boolean \u2192 bool, Record<string, V> \u2192 map<string, V>, Promise<T> \u2192 async T \u2014 and the answer lists each respelling. Refused, naming the replacement: number ("int or float?"), inline object or function types (name a value-object or a signature type), string-literal unions (name an enum), and unions mixing in a primitive or a collection.`;
48285
+ TYPE_REF_GRAMMAR = (what) => `${what}, in the neutral type grammar: a primitive (string, int, float, bool, bytes, date, datetime, duration, any; void only as a whole returns), a defined type id ("billing.Invoice", "alias::name"), list<T>, set<T>, map<K, V> (K is string, int or an enum), T? (T or no value), a union of NAMED types ("Invoice | Receipt"), and on a returns only, async T ("async void") and result<T, E> (completes with T or fails with E; "async result<void, E>"). Every named type must resolve. TypeScript and Rust spellings are accepted and respelled \u2014 "Invoice[]" \u2192 list<Invoice>, "Invoice | null" \u2192 Invoice?, boolean \u2192 bool, Record<string, V> \u2192 map<string, V>, Promise<T> \u2192 async T, Result<(), E> \u2192 result<void, E> \u2014 and the answer lists each respelling. Refused, naming the replacement: number ("int or float?"), inline object or function types (name a value-object or a signature type), string-literal unions (name an enum), and unions mixing in a primitive or a collection.`;
47960
48286
  staleServerOutput = {
47961
48287
  staleServer: import_zod12.z.boolean().optional().describe(
47962
48288
  "True when the wairon build on disk changed after this server started \u2014 the structured twin of the text answer's STALE SERVER banner. When the rebuild left the spec and tool schemas alone, writes still go through; reconnect to run the new code."
@@ -47996,6 +48322,16 @@ var init_server = __esm({
47996
48322
  ...staleServerOutput
47997
48323
  };
47998
48324
  SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
48325
+ PORTAL_TYPE_TRANSPORT = {
48326
+ HTTP_API: "HTTP",
48327
+ gRPC: "gRPC",
48328
+ GraphQL: "GraphQL",
48329
+ MessageBus: "MessageBus",
48330
+ NamedPipe: "NamedPipe",
48331
+ IPC: "IPC",
48332
+ CLI: "CLI",
48333
+ Custom: void 0
48334
+ };
47999
48335
  STATUS_VALUES = ["draft", "design", "complete"];
48000
48336
  testsToRevisitOutput = import_zod12.z.array(import_zod12.z.object({
48001
48337
  method: import_zod12.z.string().describe("The contract method this write changed or deleted."),
@@ -48110,7 +48446,7 @@ var init_server = __esm({
48110
48446
  profile: import_zod12.z.string().optional()
48111
48447
  })).describe("Requirements no installed version satisfies: nothing was written for them, and the next family run reports each as POLICY_NOT_ADOPTED."),
48112
48448
  projectType: import_zod12.z.string().optional().describe("The profile written as the new member's projectType, when exactly one adopted requirement names one."),
48113
- as: import_zod12.z.string().describe("part | project: what was created (stage 8; a part by default)."),
48449
+ as: import_zod12.z.string().describe("part | project: what was created (a part by default)."),
48114
48450
  storage: import_zod12.z.string().describe("contained | path | git: where its files live."),
48115
48451
  commit: import_zod12.z.string().optional().describe("git: the commit the member was pinned at."),
48116
48452
  ...staleServerOutput
@@ -48439,8 +48775,7 @@ function resolveInstallTargets(options) {
48439
48775
  const isPackaged = typeof process.pkg !== "undefined";
48440
48776
  const scriptPath = process.argv[1] ? path51.resolve(process.argv[1]).replace(/\\/g, "/") : null;
48441
48777
  const useDirectNode = !isPackaged && scriptPath && (scriptPath.endsWith(".js") || scriptPath.endsWith(".ts"));
48442
- const env = useGlobal ? {} : { WAIRON_PROJECT_DIR: process.cwd().replace(/\\/g, "/") };
48443
- const desiredEntry = options.hostedUrl ? hostedMcpEntry(options) : useDirectNode ? { command: "node", args: [scriptPath, "mcp", "serve"], env } : { command: "wairon", args: ["mcp", "serve"], env };
48778
+ const desiredEntry = options.hostedUrl ? hostedMcpEntry(options) : useGlobal ? machineEntry(useDirectNode ? scriptPath : null) : projectEntry(root, useDirectNode ? scriptPath : null);
48444
48779
  const existingEntry = mcpServers["wairon"];
48445
48780
  return {
48446
48781
  backend,
@@ -48455,6 +48790,16 @@ function resolveInstallTargets(options) {
48455
48790
  };
48456
48791
  });
48457
48792
  }
48793
+ function machineEntry(scriptPath) {
48794
+ return scriptPath ? { command: "node", args: [scriptPath, "mcp", "serve"], env: {} } : { command: "wairon", args: ["mcp", "serve"], env: {} };
48795
+ }
48796
+ function projectEntry(root, scriptPath) {
48797
+ if (scriptPath && !isOutsideRoot(root, scriptPath)) {
48798
+ const relative28 = path51.relative(root, scriptPath).replace(/\\/g, "/");
48799
+ return { command: "node", args: [`./${relative28}`, "mcp", "serve"] };
48800
+ }
48801
+ return { command: "wairon", args: ["mcp", "serve"] };
48802
+ }
48458
48803
  function planMcpInstall(options) {
48459
48804
  return resolveInstallTargets(options).map(({ backend, path: file, outsideProject, before, after, upToDate }) => ({
48460
48805
  backend,
@@ -48490,10 +48835,8 @@ function reportInstalled(target, agentLabel) {
48490
48835
  const wasStale = target.before !== void 0;
48491
48836
  logger.success(`wairon MCP server ${wasStale ? "updated (was stale)" : "registered"} for ${agentLabel} in ${import_chalk3.default.cyan(target.path)}.`);
48492
48837
  logger.blank();
48493
- logger.info("AI tools using this config will have access to these wairon tools:");
48494
- logger.info(" listAgents \xB7 getAgent \xB7 listDomains \xB7 validateTopology \xB7 getProjectConfig");
48495
- logger.info(" sdd_initialize_system \xB7 sdd_add_subsystem \xB7 sdd_add_component \xB7 sdd_define_interface");
48496
- logger.info(" sdd_write_narrative \xB7 sdd_validate_tree \xB7 sdd_get_status");
48838
+ logger.info("AI tools using this config get the wairon MCP server: the sdd_* spec-authoring, validation, member and");
48839
+ logger.info('rename tools, plus live agent briefs (sdd_get_agent_brief). The full list is in docs/cli.md ("MCP tools").');
48497
48840
  logger.blank();
48498
48841
  const restartApp = target.backend === "gemini" ? "Antigravity CLI (agy)" : "claude";
48499
48842
  logger.info(`Restart ${import_chalk3.default.bold(restartApp)} (or reload MCP servers) to activate.`);
@@ -48626,7 +48969,8 @@ init_skills_portal();
48626
48969
  init_base();
48627
48970
  init_fs();
48628
48971
  init_project();
48629
- var WAIRON_AGENT_FILE = /-(owner|implementer|architect)\.md$/;
48972
+ var AGENT_FILE_EXTENSION = /\.(md|ya?ml)$/;
48973
+ var WAIRON_AGENT_FILE = /-(owner|implementer|architect)\.(md|ya?ml)$/;
48630
48974
  function pruneStaleAgents(expectedPaths, scanDirs, reach2) {
48631
48975
  const dirs = new Set(scanDirs ?? [...expectedPaths].map((p) => path39.dirname(p)));
48632
48976
  let pruned = 0;
@@ -48640,7 +48984,7 @@ function pruneStaleAgents(expectedPaths, scanDirs, reach2) {
48640
48984
  continue;
48641
48985
  }
48642
48986
  for (const name of entries) {
48643
- if (!name.endsWith(".md")) continue;
48987
+ if (!AGENT_FILE_EXTENSION.test(name)) continue;
48644
48988
  const full = path39.resolve(dir, name);
48645
48989
  if (expectedPaths.has(full)) continue;
48646
48990
  let owned = WAIRON_AGENT_FILE.test(name);
@@ -48682,6 +49026,18 @@ function announceOutsideTargets(projectConfig, reach2) {
48682
49026
  }
48683
49027
  return outside.map((t) => t.type);
48684
49028
  }
49029
+ function configuredTargetTypes(projectConfig) {
49030
+ const targets = projectConfig.targets;
49031
+ return [...new Set(targets.map((t) => typeof t === "string" ? t : t.type))];
49032
+ }
49033
+ var UnknownTargetError = class extends WaironError {
49034
+ constructor(target, known) {
49035
+ super(
49036
+ `Unknown target "${target}". This project configures: ${known.length ? known.join(", ") : "(none)"}. Pass one of those, or add a target to .wai/project.yaml (\`targets\`).`
49037
+ );
49038
+ this.name = "UnknownTargetError";
49039
+ }
49040
+ };
48685
49041
  async function runGenerate(options = {}) {
48686
49042
  await generateLayer(options);
48687
49043
  const scoped = options.root || options.domain || options.domains;
@@ -48700,6 +49056,10 @@ async function generateLayer(options = {}) {
48700
49056
  assertProjectInitialized();
48701
49057
  const projectConfig = loadProjectConfig();
48702
49058
  if (!projectConfig) throw new ProjectNotInitializedError();
49059
+ if (options.target !== void 0) {
49060
+ const known = configuredTargetTypes(projectConfig);
49061
+ if (!known.includes(options.target)) throw new UnknownTargetError(options.target, known);
49062
+ }
48703
49063
  const agents = resolveAgentTopology();
48704
49064
  if (agents.length === 0) {
48705
49065
  logger.warn("No agents resolved from the spec tree. Define subsystems and components first \u2014 see `wairon status`.");
@@ -48716,6 +49076,8 @@ async function generateLayer(options = {}) {
48716
49076
  const removed = pruneStaleAgents(/* @__PURE__ */ new Set(), candidateDirs, reach2);
48717
49077
  if (removed > 0) {
48718
49078
  logger.info(`Removed ${removed} previously materialized agent file(s) \u2014 agents are served as live briefs (sdd_get_agent_brief); opt back in with rules.materializeAgentFiles: true.`);
49079
+ } else {
49080
+ logger.info("Agent files: off (rules.materializeAgentFiles) \u2014 agents are served as live briefs (sdd_get_agent_brief); no agent files written.");
48719
49081
  }
48720
49082
  }
48721
49083
  if (options.dryRun) return;
@@ -48734,6 +49096,7 @@ async function generateLayer(options = {}) {
48734
49096
  try {
48735
49097
  reinjectLocalGuides(getProjectRoot(), activeTargetTypes(projectConfig));
48736
49098
  logger.verbose("Local AI guides re-injected.");
49099
+ logger.info(`Guides, skills and context reconciled for: ${activeTargetTypes(projectConfig).join(", ") || "(no active targets)"}.`);
48737
49100
  } catch (err) {
48738
49101
  logger.warn(`Failed to re-inject AI guides: ${String(err)}`);
48739
49102
  }
@@ -48843,7 +49206,7 @@ function checkApproval(strict) {
48843
49206
  return {
48844
49207
  state: "stale",
48845
49208
  approved: false,
48846
- message: `The design, or something it was approved under (doctrine, declared inputs, \`composition\`, a direct member's approval), changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
49209
+ message: `The design, or something it was approved under, changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). ${whatMoved(record2)} Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
48847
49210
  };
48848
49211
  default:
48849
49212
  return {
@@ -48853,13 +49216,34 @@ function checkApproval(strict) {
48853
49216
  };
48854
49217
  }
48855
49218
  }
49219
+ function whatMoved(record2) {
49220
+ const diff3 = diffAgainstApproval();
49221
+ const own = diff3 ? diffSize(diff3) : null;
49222
+ const members = familyApprovals(1).filter((a) => a.parent === "" && a.as !== "part");
49223
+ const named2 = [];
49224
+ for (const m of members) {
49225
+ const name = m.alias ?? m.key;
49226
+ if (m.pinned === "unpinned") named2.push(`${name} (added since the approval)`);
49227
+ else if (m.pinned === "moved") named2.push(`${name} (its approval moved since this one was taken${m.state === "approved" ? "" : `; now ${m.state}`})`);
49228
+ else if (m.state !== "approved") named2.push(`${name} (${m.state} at its own root)`);
49229
+ }
49230
+ const current2 = new Set(members.map((m) => m.alias ?? m.key));
49231
+ for (const alias of Object.keys(record2.members ?? {})) {
49232
+ if (!current2.has(alias)) named2.push(`${alias} (removed since the approval)`);
49233
+ }
49234
+ const parts = [];
49235
+ if (own !== null && own > 0) parts.push(`${own} own spec file(s) changed since the approval`);
49236
+ if (named2.length > 0) parts.push(`direct member(s): ${named2.join(", ")}`);
49237
+ if (parts.length > 0) return `What moved: ${parts.join("; ")}.`;
49238
+ return own === null ? "This approval predates per-spec digests, so which spec moved cannot be said." : "No own spec file and no direct member's approval moved, so what changed is the doctrine, a declared input or `composition`.";
49239
+ }
48856
49240
  function upgradedCheck(record2) {
48857
49241
  const diff3 = diffAgainstApproval();
48858
49242
  const specs = !diff3 ? "This approval predates per-spec digests, so whether a spec file moved since cannot be said." : diffSize(diff3) === 0 ? "No own spec file has changed since the approval." : `${diffSize(diff3)} own spec file(s) changed since the approval.`;
48859
49243
  return {
48860
49244
  state: "stale",
48861
49245
  approved: false,
48862
- message: `The approval on record (${record2.lockedAt} by ${describeApprover(record2.lockedBy)}) was taken under an earlier gate identity \u2014 the gate identity gained inputs in stage 5: members' composition subjects, \`composition\`; code conformance moved beside the claim. ${specs} Fix: re-lock once (\`wairon lock\`) and commit the updated .wai/lock.json.`
49246
+ message: `The approval on record (${record2.lockedAt} by ${describeApprover(record2.lockedBy)}) was taken under an earlier gate identity (written by wairon ${record2.validatorVersion}). Since wairon 6.0.0 the gate identity also covers members' composition subjects and \`composition\`, and code conformance is recorded beside the claim. ${specs} Fix: re-lock once (\`wairon lock\`) and commit the updated .wai/lock.json.`
48863
49247
  };
48864
49248
  }
48865
49249
  var LockRefusedError = class extends WaironError {
@@ -48869,7 +49253,7 @@ var LockRefusedError = class extends WaironError {
48869
49253
  }
48870
49254
  };
48871
49255
  function describeMemberState(entry) {
48872
- return `${entry.alias ?? entry.key} (${entry.state}${entry.upgraded ? " \u2014 approved under the pre-stage-5 identity, re-lock it once" : ""})`;
49256
+ return `${entry.alias ?? entry.key} (${entry.state}${entry.upgraded ? " \u2014 approved under an earlier gate identity, re-lock it once" : ""})`;
48873
49257
  }
48874
49258
  function codeLine(code) {
48875
49259
  if (!code) return "code: no code analysis recorded";
@@ -48878,9 +49262,14 @@ function codeLine(code) {
48878
49262
  function summarize3(gate, members) {
48879
49263
  const diff3 = diffAgainstApproval();
48880
49264
  if (!diff3) {
48881
- logger.info("First approval of this tree \u2014 the whole spec tree becomes the approved baseline.");
49265
+ const previous = readLockRecord();
49266
+ if (previous) {
49267
+ logger.info(`Replacing the approval on record (${previous.lockedAt} by ${describeApprover(previous.lockedBy)}, wairon ${previous.validatorVersion ?? "unknown"}). It records no per-spec digests, so the whole spec tree becomes the approved baseline.`);
49268
+ } else {
49269
+ logger.info("First approval of this tree \u2014 the whole spec tree becomes the approved baseline.");
49270
+ }
48882
49271
  } else if (diffSize(diff3) === 0) {
48883
- logger.info("Nothing has changed since the last approval \u2014 this will re-validate and regenerate the agent topology.");
49272
+ logger.info("Nothing has changed since the last approval \u2014 this re-records the approval and refreshes the generated outputs of this project (guides, skills, and agent files when materialized).");
48884
49273
  } else {
48885
49274
  logger.info(`${diffSize(diff3)} spec(s) changed since the last approval:`);
48886
49275
  for (const p of diff3.changed) logger.info(` ~ ${p}`);
@@ -48894,7 +49283,7 @@ function summarize3(gate, members) {
48894
49283
  continue;
48895
49284
  }
48896
49285
  const pin2 = m.pinned === "moved" ? " \u2014 its pin moved since the last approval" : "";
48897
- logger.info(` member ${m.alias ?? m.key}: ${m.state}${m.upgraded ? " (pre-stage-5 lock, re-lock once)" : ""}${pin2}`);
49286
+ logger.info(` member ${m.alias ?? m.key}: ${m.state}${m.upgraded ? " (approved under an earlier gate identity, re-lock once)" : ""}${pin2}`);
48898
49287
  }
48899
49288
  }
48900
49289
  async function runLock(options, gate, captured) {
@@ -48908,7 +49297,7 @@ async function runLock(options, gate, captured) {
48908
49297
  );
48909
49298
  }
48910
49299
  logger.blank();
48911
- logger.warn("This records the current design as approved and (re)generates the agent topology.");
49300
+ logger.warn("This records the current design as approved in .wai/lock.json (no spec file is rewritten) and refreshes the generated outputs of this project.");
48912
49301
  if (!options.yes) {
48913
49302
  if (!process.stdin.isTTY) {
48914
49303
  logger.error("Non-interactive shell \u2014 re-run with --yes to confirm the lock.");
@@ -48918,7 +49307,7 @@ async function runLock(options, gate, captured) {
48918
49307
  {
48919
49308
  type: "confirm",
48920
49309
  name: "confirmed",
48921
- message: "Approve this design and generate the agent topology?",
49310
+ message: "Approve this design?",
48922
49311
  default: false
48923
49312
  }
48924
49313
  ]);
@@ -48942,7 +49331,7 @@ async function runLock(options, gate, captured) {
48942
49331
  const design = designOnly(gate);
48943
49332
  const count = (severity) => design.issues.filter((i) => i.severity === severity).length;
48944
49333
  const record2 = {
48945
- // The record format: 2 from stage 5 on (members, code, no children).
49334
+ // The record format: 2 since wairon 6.0.0 (members, code, no children).
48946
49335
  format: 2,
48947
49336
  stateId: captured,
48948
49337
  lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -49017,12 +49406,12 @@ function projectLabel(key, result) {
49017
49406
  const root = result.projects?.find((p) => p.key === "");
49018
49407
  return `${root?.id ?? "bound project"} (bound)`;
49019
49408
  }
49020
- function renderSpecFindings(result) {
49409
+ function renderSpecFindings(result, all = false) {
49021
49410
  const tally = { errors: false, fatalWarnings: false, waived: 0, notices: 0 };
49022
49411
  if (result.issues.length === 0) {
49023
49412
  logger.success("Spec tree is valid and component type boundaries are enforced.");
49024
49413
  }
49025
- const MAX_PRINT = 100;
49414
+ const MAX_PRINT = all ? Number.POSITIVE_INFINITY : 100;
49026
49415
  const printed = { error: 0, warning: 0, notice: 0 };
49027
49416
  const skipped = { error: 0, warning: 0, notice: 0 };
49028
49417
  for (const issue2 of result.issues) {
@@ -49042,10 +49431,11 @@ function renderSpecFindings(result) {
49042
49431
  else if (issue2.severity === "notice") logger.notice(line2);
49043
49432
  else logger.warn(line2);
49044
49433
  }
49045
- const more = (n, what) => `... and ${n} more ${what}(s) omitted. Use '--subsystem <id>' to validate a specific subsystem.`;
49434
+ const more = (n, what) => `... and ${n} more ${what}(s) not shown (${printed[what]} of ${printed[what] + n} printed). Run with --all to print every finding.`;
49046
49435
  if (skipped.error > 0) logger.error(more(skipped.error, "error"));
49047
49436
  if (skipped.warning > 0) logger.warn(more(skipped.warning, "warning"));
49048
49437
  if (skipped.notice > 0) logger.notice(more(skipped.notice, "notice"));
49438
+ if (skipped.error + skipped.warning + skipped.notice > 0 || all && result.issues.length > 100) renderCodeTotals(result);
49049
49439
  if (result.projects) {
49050
49440
  logger.blank();
49051
49441
  logger.info("Per project (each its own gate):");
@@ -49057,6 +49447,20 @@ function renderSpecFindings(result) {
49057
49447
  if (result.hint) logger.info(result.hint);
49058
49448
  return tally;
49059
49449
  }
49450
+ function renderCodeTotals(result) {
49451
+ const counts = /* @__PURE__ */ new Map();
49452
+ for (const issue2 of result.issues) {
49453
+ const key = `${issue2.severity} ${issue2.code}`;
49454
+ const entry = counts.get(key) ?? { severity: issue2.severity, code: issue2.code, n: 0 };
49455
+ entry.n++;
49456
+ counts.set(key, entry);
49457
+ }
49458
+ const rank = { error: 0, warning: 1, notice: 2 };
49459
+ const rows = [...counts.values()].sort((a, b) => (rank[a.severity] ?? 3) - (rank[b.severity] ?? 3) || b.n - a.n || a.code.localeCompare(b.code));
49460
+ logger.blank();
49461
+ logger.info("Findings by code:");
49462
+ for (const r of rows) logger.info(` ${String(r.n).padStart(6)} ${r.severity.padEnd(7)} ${r.code}`);
49463
+ }
49060
49464
  async function runValidate(options = {}) {
49061
49465
  assertProjectInitialized();
49062
49466
  const projectConfig = loadProjectConfig();
@@ -49128,7 +49532,7 @@ async function runValidate(options = {}) {
49128
49532
  };
49129
49533
  const family = selectsFamily(sddOptions, declaredMembers(projectConfig).length > 0);
49130
49534
  const sddResult = family ? validateFamily2(sddOptions) : validateProject2(sddOptions);
49131
- const tally = renderSpecFindings(sddResult);
49535
+ const tally = renderSpecFindings(sddResult, options.all === true);
49132
49536
  hasErrors ||= tally.errors;
49133
49537
  hasFatalWarnings ||= tally.fatalWarnings;
49134
49538
  waivedWarnings += tally.waived;
@@ -49236,6 +49640,9 @@ async function runStatus(options = {}) {
49236
49640
  logger.blank();
49237
49641
  if (lock.drifted) logger.warn(lock.text.trim());
49238
49642
  else logger.info(lock.text.trim());
49643
+ logger.info(import_chalk4.default.gray(
49644
+ "The percentages measure authoring readiness (each spec's draft/design/complete status), not approval: approval is the lock record above (.wai/lock.json), and `wairon lock` does not change any spec's status."
49645
+ ));
49239
49646
  }
49240
49647
  logger.blank();
49241
49648
  }
@@ -49370,14 +49777,14 @@ var http = __toESM(require("http"));
49370
49777
  var https = __toESM(require("https"));
49371
49778
  init_defaults();
49372
49779
  function downloadFile(url, dest) {
49373
- return new Promise((resolve55, reject) => {
49780
+ return new Promise((resolve56, reject) => {
49374
49781
  const file = fs31.createWriteStream(dest);
49375
49782
  const get4 = url.startsWith("https://") ? https.get : http.get;
49376
49783
  get4(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
49377
49784
  if (res.statusCode === 301 || res.statusCode === 302) {
49378
49785
  file.close();
49379
49786
  res.destroy();
49380
- downloadFile(res.headers.location, dest).then(resolve55).catch(reject);
49787
+ downloadFile(res.headers.location, dest).then(resolve56).catch(reject);
49381
49788
  return;
49382
49789
  }
49383
49790
  if (res.statusCode !== 200) {
@@ -49389,7 +49796,7 @@ function downloadFile(url, dest) {
49389
49796
  res.pipe(file);
49390
49797
  file.on("finish", () => {
49391
49798
  res.destroy();
49392
- file.close(() => resolve55());
49799
+ file.close(() => resolve56());
49393
49800
  });
49394
49801
  file.on("error", (err) => {
49395
49802
  res.destroy();
@@ -49883,7 +50290,12 @@ function writeBundle(root, resolved) {
49883
50290
  const dest = path52.join(root, ".wai", "packs", resolved.name, resolved.version);
49884
50291
  fs32.rmSync(dest, { recursive: true, force: true });
49885
50292
  fs32.mkdirSync(path52.dirname(dest), { recursive: true });
49886
- fs32.cpSync(resolved.path, dest, { recursive: true });
50293
+ if (fs32.statSync(resolved.path).isFile()) {
50294
+ fs32.mkdirSync(dest, { recursive: true });
50295
+ fs32.copyFileSync(resolved.path, path52.join(dest, `pack${path52.extname(resolved.path).toLowerCase()}`));
50296
+ } else {
50297
+ fs32.cpSync(resolved.path, dest, { recursive: true });
50298
+ }
49887
50299
  return dest;
49888
50300
  }
49889
50301
  function bundlePack(name, options = {}) {
@@ -50665,7 +51077,7 @@ async function runLock2(options) {
50665
51077
  }
50666
51078
  }
50667
51079
  if (skippedErrors > 0) {
50668
- logger.error(`... and ${skippedErrors} more error(s) omitted.`);
51080
+ logger.error(`... and ${skippedErrors} more error(s) not shown (${errorCount} of ${errors.length} printed). Run \`wairon validate --all\` to see every finding (it judges draft specs leniently; the lock judges them as complete).`);
50669
51081
  }
50670
51082
  logger.blank();
50671
51083
  logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
@@ -50717,7 +51129,7 @@ async function validateCommand(opts) {
50717
51129
  for (const t of recover3(getProjectRoot(), false)) {
50718
51130
  logger.notice(`[TRANSACTION_PENDING] an unfinished family migration (${t.verb}, transaction ${t.id}, coordinator phase ${t.phase}) \u2014 run \`wairon doctor --fix\` to roll it back`);
50719
51131
  }
50720
- await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive, family: opts.family });
51132
+ await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive, family: opts.family, all: opts.all });
50721
51133
  }
50722
51134
  async function statusCommand(opts) {
50723
51135
  const target = resolveTarget(getProjectRoot(), {});
@@ -51164,6 +51576,10 @@ var SCREENFUL = 20;
51164
51576
  function shownDir(dir) {
51165
51577
  return nodePath3.relative(process.cwd(), dir) || ".";
51166
51578
  }
51579
+ function relockLabel(dir) {
51580
+ const shown2 = shownDir(dir);
51581
+ return shown2 === "." ? "this project (.)" : shown2;
51582
+ }
51167
51583
  function printPlan(planned) {
51168
51584
  logger.header(`wairon family migration: ${planned.request.verb}`);
51169
51585
  logger.info(`Family root: ${shownDir(planned.familyRoot)}${planned.whole ? "" : " (not the whole family: the reach stops below its top)"}`);
@@ -51174,7 +51590,7 @@ function printPlan(planned) {
51174
51590
  for (const r of planned.refusals) logger.error(`[${r.code}]${r.project ? ` ${ownerLabel(r.project)}:` : ""} ${r.detail}`);
51175
51591
  for (const note of planned.notes) logger.warn(`NOTE: ${note}`);
51176
51592
  printFiles(planned);
51177
- if (planned.relock.length > 0) logger.info(`To re-lock once applied: ${planned.relock.map(shownDir).join(", ")}`);
51593
+ if (planned.relock.length > 0) logger.info(`To re-lock once applied: ${planned.relock.map(relockLabel).join(", ")}`);
51178
51594
  }
51179
51595
  function printFiles(planned) {
51180
51596
  if (planned.changes.length === 0) return;
@@ -51203,7 +51619,7 @@ function printOutcome(report4) {
51203
51619
  const verb = report4.plan.request.verb;
51204
51620
  if (report4.applied) {
51205
51621
  logger.success(`Applied the ${verb} migration: ${report4.plan.changes.length} file(s) across ${new Set(report4.plan.changes.map((c) => c.project)).size} project(s).`);
51206
- for (const dir of report4.relock) logger.info(`Re-lock ${shownDir(dir)}: run \`wairon lock\` there.`);
51622
+ for (const dir of report4.relock) logger.info(`Re-lock ${relockLabel(dir)}: run \`wairon lock\` ${shownDir(dir) === "." ? "here" : "there"}.`);
51207
51623
  return;
51208
51624
  }
51209
51625
  process.exitCode = 1;
@@ -51270,9 +51686,14 @@ async function runInit(options = {}) {
51270
51686
  const ancestorRoot = findSystemRoot(cwd);
51271
51687
  if (ancestorRoot && path58.resolve(ancestorRoot) === path58.resolve(cwd)) {
51272
51688
  logger.info("Project already initialized.");
51273
- logger.info("Design your spec tree with the SDD architect skill, then run `wairon generate`.");
51689
+ logger.info("Design your spec tree with the SDD architect skill, validate it, then run `wairon lock` to approve it.");
51274
51690
  return;
51275
51691
  }
51692
+ if (!options.yes && !process.stdin.isTTY) {
51693
+ throw new WaironError(
51694
+ "Non-interactive shell: `wairon init` asks questions and there is no terminal to ask them on. Re-run with --yes (-y) to use the defaults (project name from the directory, profile backend, targets claude and agy). Nothing was written."
51695
+ );
51696
+ }
51276
51697
  if (ancestorRoot) {
51277
51698
  await runInitAsMember(ancestorRoot, cwd, options);
51278
51699
  return;
@@ -51541,13 +51962,17 @@ async function executeInit(projectName, projectConfig, guidePlan) {
51541
51962
  logger.warn(`Failed to automatically register MCP server for Claude: ${err}`);
51542
51963
  }
51543
51964
  }
51544
- if (targetTypes.includes("gemini") || targetTypes.includes("agy")) {
51965
+ if (targetTypes.includes("gemini")) {
51545
51966
  try {
51546
51967
  const { runMcpInstall: runMcpInstall2 } = (init_mcp(), __toCommonJS(mcp_exports));
51547
51968
  runMcpInstall2({ backend: "gemini", global: false });
51548
51969
  } catch (err) {
51549
- logger.warn(`Failed to automatically register MCP server for Antigravity: ${err}`);
51970
+ logger.warn(`Failed to automatically register MCP server for the Gemini CLI: ${err}`);
51550
51971
  }
51972
+ } else if (targetTypes.includes("agy")) {
51973
+ logger.info(
51974
+ `Antigravity (agy) reads MCP servers only from its machine-wide ~/.gemini/antigravity-cli/mcp_config.json, which init does not write. To give it the wairon tools: ${import_chalk11.default.bold("wairon mcp install --backend gemini --global")}`
51975
+ );
51551
51976
  }
51552
51977
  syncContextFiles();
51553
51978
  logger.verbose("Context files seeded in .wai/context/");
@@ -51571,9 +51996,10 @@ async function executeInit(projectName, projectConfig, guidePlan) {
51571
51996
  logger.blank();
51572
51997
  logger.info("Next steps:");
51573
51998
  logger.info(" Edit .wai/context/project.md \u2014 describe the project concept and stack details for the AI");
51574
- logger.info(" wairon status \u2014 view the architecture completeness dashboard");
51575
- logger.info(" wairon validate \u2014 check the spec tree & component boundaries");
51576
51999
  logger.info(" Design your spec tree with the SDD architect skill or the wairon MCP sdd_* tools");
52000
+ logger.info(" wairon status \u2014 view the spec tree, its authoring readiness and approval state");
52001
+ logger.info(" wairon validate \u2014 check the spec tree & component boundaries");
52002
+ logger.info(" wairon lock \u2014 approve the validated design (commit .wai/lock.json); `wairon lock-check` gates CI on it");
51577
52003
  logger.blank();
51578
52004
  }
51579
52005
  function buildProjectConfig(name, targets, now, projectType) {
@@ -51656,11 +52082,13 @@ Define the non-negotiable architectural guardrails here. The AI agent must follo
51656
52082
 
51657
52083
  * [ ] **Primary Language & Runtime:** Node.js (TypeScript) / Python / etc.
51658
52084
  * [ ] **Architectural Style:** Clean Architecture / Hexagonal / Domain-Driven Design (DDD).
51659
- * [ ] **Data Persistence Rules:** E.g. No raw SQL in controllers; all DB operations must use a \`Store\` / \`Repository\`.
51660
- * [ ] **Stereotype Dependencies:**
51661
- * \`Store\` components can only call other \`Stores\` or \`Registries\`.
51662
- * \`Adapter\` components cannot depend on \`Orchestrators\` or \`Stores\` directly.
51663
- * Only \`Portal\` components can accept external traffic.
52085
+ * [ ] **Data Persistence Rules:** E.g. No raw SQL in controllers; held state lives in a data component \u2014 the recommended \`Repository\` (owning its \`Store\`, \`Registry\`, \`Index\`es and \`Query\`s), or a deliberately standalone \`Store\` for genuinely simple state.
52086
+ * [ ] **Stereotype Dependencies** (enforced by \`sdd_validate_tree\`):
52087
+ * Only \`Portal\` components accept external traffic; a gateway is a \`Portal\` with the \`gateway\` variant.
52088
+ * A \`Portal\` never depends on a \`Store\`, \`Registry\` or \`Adapter\`. Its reads may go through a \`Repository\` / \`Index\` facade; every write routes through an \`Orchestrator\`.
52089
+ * State never lives as fields inside an \`Orchestrator\`; logic is an \`Orchestrator\` (\`dependencyClass: pure | read\` bounds what it may depend on).
52090
+ * A \`Registry\` and an \`Index\` depend on the \`Store\` they read; a \`Store\` may use an \`Adapter\` as its backend.
52091
+ * Outbound calls to third parties go through an \`Adapter\`.
51664
52092
 
51665
52093
  ---
51666
52094
 
@@ -51673,15 +52101,16 @@ Define the non-negotiable architectural guardrails here. The AI agent must follo
51673
52101
 
51674
52102
  ---
51675
52103
 
51676
- ## Stage 3: Ingress/Egress Portals (Level 2 & Level 3)
52104
+ ## Stage 3: Ingress Portals & Outbound Adapters (Level 2 & Level 3)
51677
52105
 
51678
- Portals are the boundaries of your subsystems. Define how requests enter and leave.
52106
+ Portals are where requests enter a subsystem; Adapters are where calls leave it.
51679
52107
 
51680
52108
  * [ ] **Define Ingress Portals (REST / gRPC / MessageBus):**
51681
52109
  * *AI Action:* Create L2 Portal components with \`status: draft\` and map their L3 interfaces.
51682
- * *Design check:* Ensure HTTP endpoints (method, path) or gRPC names are correctly declared in the method bindings.
51683
- * [ ] **Define Egress Portals (Clients / Publishers):**
51684
- * *AI Action:* Declare any external event publishing or client communication Portals.
52110
+ * *Design check:* Bind each method's wire endpoint with \`sdd_set_endpoints\`, and give every HTTP Portal its \`mounts\` (or a listener) \u2014 otherwise \`UNMOUNTED_PORTAL\`.
52111
+ * *Design check:* A subsystem another subsystem calls lists those callers in its \`publicInterfaces[].consumers\`.
52112
+ * [ ] **Define Outbound Adapters (third-party clients / publishers):**
52113
+ * *AI Action:* Declare an \`Adapter\` for each external API, database driver or event publisher the subsystem calls.
51685
52114
 
51686
52115
  ---
51687
52116
 
@@ -51706,13 +52135,13 @@ Map the behavior step-by-step.
51706
52135
 
51707
52136
  ---
51708
52137
 
51709
- ## Stage 6: Sandbox Implementation
52138
+ ## Stage 6: Approval & Implementation
51710
52139
 
51711
- Once the specs are clean and compiled, mark the components as \`status: complete\` to lock them, then generate the agents and write code!
52140
+ Once the specs validate cleanly, the human approves the design with \`wairon lock\`; implementation starts from the approved design.
51712
52141
 
51713
52142
  * [ ] **Validation Check (AI):** Call the \`sdd_validate_tree\` MCP tool (must return 0 errors).
51714
- * [ ] **Agent Generation (human):** The developer runs \`wairon generate\` from their terminal to instantiate agent sandboxes \u2014 the AI does not run this.
51715
- * [ ] **Code Implementation:** Let the agent implement the component code matching the narrative.
52143
+ * [ ] **Approval (human):** The developer runs \`wairon lock\` from their terminal and commits \`.wai/lock.json\` \u2014 the AI does not run this. The lock records the approval; it does not rewrite spec files or their \`status\`. \`wairon lock-check\` gates merges on it in CI.
52144
+ * [ ] **Code Implementation (AI):** Confirm \`sdd_get_status\` reports the design approved, then delegate each component via the \`sdd-delegate\` skill (live briefs from \`sdd_get_agent_brief\`); code must match the L3 interfaces and L5 narratives.
51716
52145
  `;
51717
52146
  writeFile(fromProjectRoot(".wai", "phased_design.md"), content);
51718
52147
  }
@@ -52013,7 +52442,7 @@ function releaseChannelLabel(tag) {
52013
52442
  return version.slice(dash + 1).replace(/\.\d+$/, "") || "stable";
52014
52443
  }
52015
52444
  function fetchReleases(repo) {
52016
- return new Promise((resolve55, reject) => {
52445
+ return new Promise((resolve56, reject) => {
52017
52446
  const url = `https://api.github.com/repos/${repo}/releases?per_page=100`;
52018
52447
  const options = {
52019
52448
  headers: {
@@ -52033,7 +52462,7 @@ function fetchReleases(repo) {
52033
52462
  return;
52034
52463
  }
52035
52464
  try {
52036
- resolve55(JSON.parse(data));
52465
+ resolve56(JSON.parse(data));
52037
52466
  } catch {
52038
52467
  reject(new Error("Failed to parse GitHub API response"));
52039
52468
  }
@@ -52205,7 +52634,7 @@ async function filteredCheckbox(config) {
52205
52634
  32,
52206
52635
  Math.max(...items.map((i) => i.label.length))
52207
52636
  );
52208
- return new Promise((resolve55) => {
52637
+ return new Promise((resolve56) => {
52209
52638
  const checked = /* @__PURE__ */ new Set();
52210
52639
  let cursor = 0;
52211
52640
  let filterIdx = 0;
@@ -52272,7 +52701,7 @@ async function filteredCheckbox(config) {
52272
52701
  );
52273
52702
  teardown();
52274
52703
  const result = items.filter((_, idx) => checked.has(idx)).map((i) => i.value);
52275
- resolve55(result);
52704
+ resolve56(result);
52276
52705
  }
52277
52706
  function abort() {
52278
52707
  process.stdout.write("\n");
@@ -52736,7 +53165,7 @@ function mcpEntryHealth(settingsPath) {
52736
53165
  if (!entry) return { mark: "warn", note: "not registered" };
52737
53166
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
52738
53167
  const scriptPath = entry.args[0];
52739
- if (!fs38.existsSync(scriptPath)) {
53168
+ if (!fs38.existsSync(path61.resolve(getProjectRoot(), scriptPath))) {
52740
53169
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
52741
53170
  }
52742
53171
  }
@@ -52802,15 +53231,26 @@ async function runDoctor(options = {}) {
52802
53231
  try {
52803
53232
  const cfg = loadProjectConfig();
52804
53233
  if (!cfg) throw new ProjectNotInitializedError();
52805
- const result = validateProject2({ rules: cfg.rules, projectType: cfg.projectType });
52806
- const errs = result.issues.filter((i) => i.severity === "error").length;
52807
- const warns = result.issues.filter((i) => i.severity === "warning").length;
52808
- const notes = result.issues.filter((i) => i.severity === "notice").length;
53234
+ const result = validateFamily2({ rules: cfg.rules, projectType: cfg.projectType });
53235
+ const counts = severityCounts(result.issues);
53236
+ const { errs, warns, notes } = counts;
52809
53237
  const noted = notes > 0 ? `, ${notes} notice(s)` : "";
52810
53238
  if (errs > 0) line(tally, "error", `Conformance: ${errs} error(s), ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52811
53239
  else if (warns > 0) line(tally, "warn", `Conformance: ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52812
53240
  else if (notes > 0) line(tally, "ok", `Conformance: 0 errors, 0 warnings, ${notes} notice(s) \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52813
53241
  else line(tally, "ok", "Conformance: 0 errors, 0 warnings");
53242
+ const byProject = /* @__PURE__ */ new Map();
53243
+ for (const issue2 of result.issues) {
53244
+ const key = issue2.project ?? "";
53245
+ byProject.set(key, [...byProject.get(key) ?? [], issue2]);
53246
+ }
53247
+ if (byProject.size > 1 || byProject.size === 1 && !byProject.has("")) {
53248
+ for (const [key, issues] of [...byProject].sort(([a], [b]) => a.localeCompare(b))) {
53249
+ const own = severityCounts(issues);
53250
+ if (own.errs === 0 && own.warns === 0 && own.notes === 0) continue;
53251
+ console.log(` ${key === "" ? "this project" : `member ${key}`}: ${own.errs} error(s), ${own.warns} warning(s), ${own.notes} notice(s)`);
53252
+ }
53253
+ }
52814
53254
  } catch (e) {
52815
53255
  line(tally, "warn", `Could not run conformance check: ${e instanceof Error ? e.message : String(e)}`);
52816
53256
  }
@@ -52880,6 +53320,11 @@ async function runDoctor(options = {}) {
52880
53320
  line(tally, "warn", `${proposal.kind} ${proposal.specId} ${proposal.path}: "${proposal.written}" \u2192 ${proposal.stored} (proposed)`);
52881
53321
  }
52882
53322
  }
53323
+ const enumProposals = spellings.flatMap((repair) => repair.enumProposals);
53324
+ if (enumProposals.length > 0) {
53325
+ console.log(` ${import_chalk17.default.gray("enum proposed \u2014 never written: define the enum, then write its id here")}`);
53326
+ for (const proposal of enumProposals) line(tally, "warn", describeEnumProposal(proposal));
53327
+ }
52883
53328
  if (authorNeeded.length > 0) {
52884
53329
  console.log(` ${import_chalk17.default.gray("needs an author \u2014 no rewrite can settle these")}`);
52885
53330
  for (const problem of authorNeeded) {
@@ -52890,6 +53335,11 @@ async function runDoctor(options = {}) {
52890
53335
  } catch (e) {
52891
53336
  line(tally, "warn", `Could not plan the type-spelling repair: ${e instanceof Error ? e.message : String(e)}`);
52892
53337
  }
53338
+ try {
53339
+ reportMemberRepairs(tally);
53340
+ } catch (e) {
53341
+ line(tally, "warn", `Could not plan the members' repairs: ${e instanceof Error ? e.message : String(e)}`);
53342
+ }
52893
53343
  try {
52894
53344
  const planned = plan5({ verb: "chaining", rehearse: false });
52895
53345
  const pending4 = planned.chaining;
@@ -52958,7 +53408,7 @@ async function runDoctor(options = {}) {
52958
53408
  line(
52959
53409
  tally,
52960
53410
  "warn",
52961
- `stale \u2014 the lock of ${lock.record.lockedAt} was taken under an earlier gate identity: the gate identity gained inputs in stage 5 (members' composition subjects, \`composition\`; code conformance moved beside the claim). Re-lock once (\`wairon lock\`).`
53411
+ `stale \u2014 the lock of ${lock.record.lockedAt} was taken under an earlier gate identity: the gate identity gained inputs in this release (members' composition subjects, \`composition\`; code conformance moved beside the claim). Re-lock once (\`wairon lock\`).`
52962
53412
  );
52963
53413
  } else if (lock.state === "stale") {
52964
53414
  console.log(import_chalk17.default.bold("Lock"));
@@ -53023,9 +53473,13 @@ async function runDoctor(options = {}) {
53023
53473
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
53024
53474
  }
53025
53475
  if (wantGemini) {
53026
- const globalCfg = path61.join(os13.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
53027
- const hg = mcpEntryHealth(globalCfg);
53028
- line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
53476
+ const agyHome = path61.join(os13.homedir(), ".gemini", "antigravity-cli");
53477
+ if (!fs38.existsSync(agyHome)) {
53478
+ line(tally, "ok", `Antigravity: not installed on this machine (no ${agyHome}) \u2014 nothing to register. Once it is: \`wairon mcp install --backend gemini --global\``);
53479
+ } else {
53480
+ const hg = mcpEntryHealth(path61.join(agyHome, "mcp_config.json"));
53481
+ line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
53482
+ }
53029
53483
  const projPath = fromProjectRoot(".gemini", "settings.json");
53030
53484
  if (fs38.existsSync(projPath)) {
53031
53485
  const hp = mcpEntryHealth(projPath);
@@ -53061,14 +53515,7 @@ async function applyFixes(options, tally) {
53061
53515
  } catch (e) {
53062
53516
  console.log(` ${icon("error")} Regeneration failed: ${e instanceof Error ? e.message : String(e)}`);
53063
53517
  }
53064
- try {
53065
- const pinned = pinInstalledPacksAsSelections();
53066
- if (pinned.length > 0) {
53067
- console.log(` ${icon("ok")} Recorded ${pinned.length} installed pack(s) as explicit selections: ${pinned.join(", ")}.`);
53068
- }
53069
- } catch (e) {
53070
- console.log(` ${icon("error")} Could not record pack selections: ${e instanceof Error ? e.message : String(e)}`);
53071
- }
53518
+ pinPackSelections("");
53072
53519
  try {
53073
53520
  const legacySpecs = findLegacySpecFiles();
53074
53521
  if (legacySpecs.length > 0) {
@@ -53088,63 +53535,157 @@ async function applyFixes(options, tally) {
53088
53535
  } catch (e) {
53089
53536
  console.log(` ${icon("error")} Chained-subproject backfill failed: ${e instanceof Error ? e.message : String(e)}`);
53090
53537
  }
53538
+ repairProjectSpecs("");
53539
+ await migrateChaining(options, tally);
53540
+ repairMembers();
53541
+ await repairMcpRegistration(options, targets);
53542
+ logger.blank();
53543
+ }
53544
+ function whoPrefix(who) {
53545
+ return who === "" ? "" : `[${who}] `;
53546
+ }
53547
+ function severityCounts(issues) {
53548
+ return {
53549
+ errs: issues.filter((i) => i.severity === "error").length,
53550
+ warns: issues.filter((i) => i.severity === "warning").length,
53551
+ notes: issues.filter((i) => i.severity === "notice").length
53552
+ };
53553
+ }
53554
+ function describeEnumProposal(p) {
53555
+ return `${p.kind} ${p.specId} ${p.path}: "${p.written}" \u2192 enum ${p.enumId} (${p.values.join(", ")}), written as ${p.enumId}${p.optional ? "?" : ""} (proposed)`;
53556
+ }
53557
+ function pinPackSelections(who) {
53558
+ try {
53559
+ const pinned = pinInstalledPacksAsSelections();
53560
+ if (pinned.length === 0) return;
53561
+ console.log(` ${icon("ok")} ${whoPrefix(who)}Recorded ${pinned.length} installed pack(s) as explicit selections: ${pinned.join(", ")}.`);
53562
+ const pinnedNames = new Set(pinned.map((label7) => label7.replace(/@[^@]*$/, "")));
53563
+ const unobtainable = (loadProjectConfig()?.extensions?.packs ?? []).filter((e) => typeof e !== "string").filter((e) => pinnedNames.has(e.name) && !e.source && !e.bundle).map((e) => e.name);
53564
+ if (unobtainable.length > 0) {
53565
+ console.log(` ${icon("warn")} ${whoPrefix(who)}${unobtainable.length} of them record no fetchable source (installed from a local path), so CI and a fresh clone cannot obtain them: ${unobtainable.join(", ")}. Run \`wairon pack bundle --all\` to commit a copy the repository carries, or \`wairon pack use <name> --source <url>\` to record where \`wairon pack sync\` fetches each.`);
53566
+ }
53567
+ } catch (e) {
53568
+ console.log(` ${icon("error")} ${whoPrefix(who)}Could not record pack selections: ${e instanceof Error ? e.message : String(e)}`);
53569
+ }
53570
+ }
53571
+ function repairProjectSpecs(who) {
53572
+ const at = whoPrefix(who);
53091
53573
  try {
53092
53574
  const retirement = retireSpecialists(true);
53093
53575
  if (retirement.retyped.length > 0) {
53094
- console.log(` ${icon("ok")} Retired ${retirement.retyped.length} Specialist(s) to Orchestrator.`);
53576
+ console.log(` ${icon("ok")} ${at}Retired ${retirement.retyped.length} Specialist(s) to Orchestrator.`);
53095
53577
  }
53096
53578
  if (retirement.rebasedVariants.length > 0) {
53097
- console.log(` ${icon("ok")} Rebased ${retirement.rebasedVariants.length} variant(s) from Specialist to Orchestrator: ${retirement.rebasedVariants.join(", ")}.`);
53579
+ console.log(` ${icon("ok")} ${at}Rebased ${retirement.rebasedVariants.length} variant(s) from Specialist to Orchestrator: ${retirement.rebasedVariants.join(", ")}.`);
53098
53580
  }
53099
53581
  } catch (e) {
53100
- console.log(` ${icon("error")} Specialist retirement failed: ${e instanceof Error ? e.message : String(e)}`);
53582
+ console.log(` ${icon("error")} ${at}Specialist retirement failed: ${e instanceof Error ? e.message : String(e)}`);
53101
53583
  }
53102
53584
  try {
53103
53585
  const repairs = repairForeignStepFields(true);
53104
53586
  if (repairs.length > 0) {
53105
- console.log(` ${icon("ok")} Dropped foreign fields from ${repairs.length} narrative step(s):`);
53587
+ console.log(` ${icon("ok")} ${at}Dropped foreign fields from ${repairs.length} narrative step(s):`);
53106
53588
  for (const repair of repairs) {
53107
53589
  console.log(` ${repair.implementation}.${repair.method} step ${repair.stepNumber} (${repair.stepType}): ${repair.fields.join(", ")}`);
53108
53590
  }
53109
53591
  }
53110
53592
  } catch (e) {
53111
- console.log(` ${icon("error")} Narrative-step repair failed: ${e instanceof Error ? e.message : String(e)}`);
53593
+ console.log(` ${icon("error")} ${at}Narrative-step repair failed: ${e instanceof Error ? e.message : String(e)}`);
53112
53594
  }
53113
53595
  try {
53114
53596
  const signatures = repairSignatures(true);
53115
53597
  if (signatures.length > 0) {
53116
- console.log(` ${icon("ok")} Rewrote ${signatures.length} spec(s) into their stored signature form:`);
53598
+ console.log(` ${icon("ok")} ${at}Rewrote ${signatures.length} spec(s) into their stored signature form:`);
53117
53599
  for (const repair of signatures) {
53118
53600
  console.log(` ${repair.kind} ${repair.specId}: ${describeSignatureRepair(repair)}`);
53119
53601
  }
53120
53602
  }
53121
53603
  } catch (e) {
53122
- console.log(` ${icon("error")} Signature repair failed: ${e instanceof Error ? e.message : String(e)}`);
53604
+ console.log(` ${icon("error")} ${at}Signature repair failed: ${e instanceof Error ? e.message : String(e)}`);
53123
53605
  }
53124
53606
  try {
53125
53607
  const spellings = repairTypeSpellings(true);
53126
53608
  const rewritten = spellings.filter((repair) => repair.rewritten.length > 0);
53127
53609
  if (rewritten.length > 0) {
53128
53610
  const positions = rewritten.reduce((n, repair) => n + repair.rewritten.length, 0);
53129
- console.log(` ${icon("ok")} Rewrote ${positions} type position(s) in ${rewritten.length} spec(s) into their canonical spelling.`);
53611
+ console.log(` ${icon("ok")} ${at}Rewrote ${positions} type position(s) in ${rewritten.length} spec(s) into their canonical spelling.`);
53130
53612
  }
53131
53613
  const proposals = spellings.flatMap((repair) => repair.proposals);
53132
53614
  if (proposals.length > 0) {
53133
- console.log(` ${icon("warn")} ${proposals.length} number position(s) with int proposed \u2014 not written; confirm by writing int, or write float:`);
53615
+ console.log(` ${icon("warn")} ${at}${proposals.length} number position(s) with int proposed \u2014 not written; confirm by writing int, or write float:`);
53134
53616
  for (const proposal of proposals) console.log(` ${proposal.kind} ${proposal.specId} ${proposal.path}: "${proposal.written}" \u2192 ${proposal.stored} (proposed)`);
53135
53617
  }
53618
+ const enumProposals = spellings.flatMap((repair) => repair.enumProposals);
53619
+ if (enumProposals.length > 0) {
53620
+ console.log(` ${icon("warn")} ${at}${enumProposals.length} string-literal union(s) with an enum proposed \u2014 not written; define the enum (sdd_add_type kind enum), then write its id:`);
53621
+ for (const proposal of enumProposals) console.log(` ${describeEnumProposal(proposal)}`);
53622
+ }
53136
53623
  const authorNeeded = spellings.flatMap((repair) => repair.authorNeeded);
53137
53624
  if (authorNeeded.length > 0) {
53138
- console.log(` ${icon("warn")} ${authorNeeded.length} type position(s) need an author \u2014 no rewrite can settle them:`);
53625
+ console.log(` ${icon("warn")} ${at}${authorNeeded.length} type position(s) need an author \u2014 no rewrite can settle them:`);
53139
53626
  for (const problem of authorNeeded) {
53140
53627
  console.log(` ${problem.kind} ${problem.specId} ${problem.path ?? ""}: "${problem.written}" \u2014 ${problem.replacement ? `write ${problem.replacement}` : problem.detail}`);
53141
53628
  }
53142
53629
  }
53143
53630
  } catch (e) {
53144
- console.log(` ${icon("error")} Type-spelling repair failed: ${e instanceof Error ? e.message : String(e)}`);
53631
+ console.log(` ${icon("error")} ${at}Type-spelling repair failed: ${e instanceof Error ? e.message : String(e)}`);
53145
53632
  }
53146
- await migrateChaining(options, tally);
53147
- await repairMcpRegistration(options, targets);
53633
+ }
53634
+ function memberProjects() {
53635
+ const root = getProjectRoot();
53636
+ return graph().nodes.filter((node) => node.namespace !== "" && node.directory && pathExists(node.directory)).map((node) => ({ alias: node.mountAlias ?? node.namespace, directory: node.directory, inside: !isOutsideRoot(root, node.directory) }));
53637
+ }
53638
+ function repairMembers() {
53639
+ let members;
53640
+ try {
53641
+ members = memberProjects();
53642
+ } catch (e) {
53643
+ console.log(` ${icon("error")} Could not read the family's members: ${e instanceof Error ? e.message : String(e)}`);
53644
+ return;
53645
+ }
53646
+ for (const member of members) {
53647
+ if (!member.inside) {
53648
+ console.log(` ${icon("warn")} [${member.alias}] lies outside this project (${member.directory}): run \`wairon doctor --fix\` there to upgrade it.`);
53649
+ continue;
53650
+ }
53651
+ runWithProjectRoot(member.directory, () => {
53652
+ if (!projectConfigExists()) return;
53653
+ pinPackSelections(member.alias);
53654
+ repairProjectSpecs(member.alias);
53655
+ });
53656
+ }
53657
+ }
53658
+ function reportMemberRepairs(tally) {
53659
+ const pending4 = [];
53660
+ for (const member of memberProjects()) {
53661
+ const counts = runWithProjectRoot(member.directory, () => {
53662
+ if (!projectConfigExists()) return null;
53663
+ const spellings = repairTypeSpellings(false);
53664
+ return {
53665
+ specialists: retireSpecialists(false).retyped.length,
53666
+ stepFields: repairForeignStepFields(false).length,
53667
+ signatures: repairSignatures(false).length,
53668
+ respell: spellings.reduce((n, r) => n + r.rewritten.length, 0),
53669
+ proposals: spellings.reduce((n, r) => n + r.proposals.length + r.enumProposals.length, 0),
53670
+ authorNeeded: spellings.reduce((n, r) => n + r.authorNeeded.length, 0)
53671
+ };
53672
+ });
53673
+ if (!counts) continue;
53674
+ const parts = [
53675
+ counts.specialists ? `${counts.specialists} Specialist(s)` : "",
53676
+ counts.stepFields ? `${counts.stepFields} step(s) with foreign fields` : "",
53677
+ counts.signatures ? `${counts.signatures} spec(s) with stale signatures` : "",
53678
+ counts.respell ? `${counts.respell} type position(s) to respell` : "",
53679
+ counts.proposals ? `${counts.proposals} proposal(s)` : "",
53680
+ counts.authorNeeded ? `${counts.authorNeeded} position(s) needing an author` : ""
53681
+ ].filter(Boolean);
53682
+ if (parts.length === 0) continue;
53683
+ const where = member.inside ? "`wairon doctor --fix` here cascades into it" : `run \`wairon doctor --fix\` in ${member.directory}`;
53684
+ pending4.push(`member ${member.alias}: ${parts.join(", ")} \u2014 ${where}`);
53685
+ }
53686
+ if (pending4.length === 0) return;
53687
+ console.log(import_chalk17.default.bold("Members"));
53688
+ for (const text3 of pending4) line(tally, "warn", text3);
53148
53689
  logger.blank();
53149
53690
  }
53150
53691
  async function repairMcpRegistration(options, targets) {
@@ -53239,11 +53780,11 @@ function reportLabel(key) {
53239
53780
  return key === "" ? "(this project)" : key;
53240
53781
  }
53241
53782
  function printUpgradeReport(report4) {
53242
- console.log(import_chalk17.default.bold("Composed validation: what stage 4 changed"));
53783
+ console.log(import_chalk17.default.bold("Composed validation: what it changed since each lock"));
53243
53784
  console.log(import_chalk17.default.gray(" A lock records totals only (errors, warnings, notices) and the validator version that took it \u2014 nothing per code \u2014 so this is not a per-code diff. Today's findings are classed by a reason computed now."));
53244
53785
  for (const p of report4.projects) {
53245
53786
  const locked = p.lockedTotals ? `locked by v${p.lockedBy}: ${p.lockedTotals}` : "never locked";
53246
- const attribution = p.lockedTotals ? p.predatesStage4 ? " \u2014 the lock predates stage 4, so a difference is the upgrade's" : " \u2014 the lock was taken by a stage-4 validator, so a difference is not the upgrade's" : "";
53787
+ const attribution = p.lockedTotals ? p.predatesStage4 ? " \u2014 the lock predates composed validation, so a difference is the upgrade's" : " \u2014 the lock was taken under composed validation, so a difference is not the upgrade's" : "";
53247
53788
  console.log(` ${reportLabel(p.key)}: ${locked}; today (as-complete): ${p.currentTotals}${attribution}`);
53248
53789
  }
53249
53790
  const byCode = /* @__PURE__ */ new Map();
@@ -53252,12 +53793,12 @@ function printUpgradeReport(report4) {
53252
53793
  console.log(` ${import_chalk17.default.bold(code)} (${entries.length})`);
53253
53794
  for (const e of entries) {
53254
53795
  const where = `${reportLabel(e.project)}${e.specId ? ` ${e.specId}` : ""}`;
53255
- const reason = e.reason === "escalated" ? "escalated: a stage-2 notice that is an error since stage 4" : e.reason === "pinned" ? "pinned: judged against the project's own pin, no longer through the parent's live tree" : `positional: the family's top matches it${e.resolvedAs ? ` to ${e.resolvedAs}` : ""} by position, which is how it passed before${e.rewrite ? ` \u2014 the migration writes ${e.rewrite}` : ""}`;
53796
+ const reason = e.reason === "escalated" ? "escalated: once a notice, an error under composed validation" : e.reason === "pinned" ? "pinned: judged against the project's own pin, no longer through the parent's live tree" : `positional: the family's top matches it${e.resolvedAs ? ` to ${e.resolvedAs}` : ""} by position, which is how it passed before${e.rewrite ? ` \u2014 the migration writes ${e.rewrite}` : ""}`;
53256
53797
  console.log(` ${e.severity} ${where}: ${reason}`);
53257
53798
  }
53258
53799
  }
53259
- if (report4.entries.length === 0) console.log(" No finding is new for a reason stage 4 introduced.");
53260
- console.log(` ${report4.unclassified} finding(s) could not be attributed to stage 4.`);
53800
+ if (report4.entries.length === 0) console.log(" No finding is new for a reason composed validation introduced.");
53801
+ console.log(` ${report4.unclassified} finding(s) could not be attributed to composed validation.`);
53261
53802
  if (report4.unmatched.length > 0) {
53262
53803
  console.log(` ${import_chalk17.default.bold("Positional matches no rule decides")} (${report4.unmatched.length}) \u2014 counted above; a person picks:`);
53263
53804
  for (const m of report4.unmatched) {
@@ -53421,7 +53962,7 @@ function printProjectMigration(p) {
53421
53962
  console.log(` will be removed from its stored L0 by the first write (the schema does not know them): ${p.droppedKeys.join("; ")}`);
53422
53963
  }
53423
53964
  for (const x of p.externals) {
53424
- const why = x.reason === "legacy-pin" ? "replaces the stage-1 family pin of" : "its references cross into";
53965
+ const why = x.reason === "legacy-pin" ? "replaces the legacy family pin of" : "its references cross into";
53425
53966
  console.log(` external: ${x.alias}: {} \u2014 ${why} ${projectLabel2(x.producer)}`);
53426
53967
  }
53427
53968
  for (const i of p.imports) {
@@ -53437,10 +53978,10 @@ function printProjectMigration(p) {
53437
53978
  if (m.retired.length > 0) console.log(` retires with the mount: ${m.retired.join("; ")}`);
53438
53979
  }
53439
53980
  if (p.supersededPins.length > 0) {
53440
- console.log(` superseded family pins: ${p.supersededPins.length} \u2014 deleted once the externals that replace them are pinned; nothing reads them since stage 3`);
53981
+ console.log(` superseded family pins: ${p.supersededPins.length} \u2014 deleted once the externals that replace them are pinned; nothing reads them any more`);
53441
53982
  }
53442
53983
  for (const alias of p.locations) {
53443
- console.log(` member: ${alias}: the deprecated long-form \`path\` rewritten to \`source\` (stage 8) \u2014 its meaning unchanged`);
53984
+ console.log(` member: ${alias}: the deprecated long-form \`path\` rewritten to \`source\` \u2014 its meaning unchanged`);
53444
53985
  }
53445
53986
  }
53446
53987
  function describeEntry(e) {
@@ -53510,13 +54051,13 @@ function writeArtifact(dest, content) {
53510
54051
  fs39.writeFileSync(dest, content, "utf-8");
53511
54052
  }
53512
54053
  function parseSequenceRef(ref) {
53513
- const sep18 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
53514
- if (sep18 <= 0 || sep18 === ref.length - 1) {
54054
+ const sep19 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
54055
+ if (sep19 <= 0 || sep19 === ref.length - 1) {
53515
54056
  throw new WaironError(
53516
54057
  `Invalid --sequence reference "${ref}". Use <componentId>:<methodName> (e.g. billing-portal:authorize).`
53517
54058
  );
53518
54059
  }
53519
- return { component: ref.slice(0, sep18), method: ref.slice(sep18 + 1) };
54060
+ return { component: ref.slice(0, sep19), method: ref.slice(sep19 + 1) };
53520
54061
  }
53521
54062
  function canvasRelations(options) {
53522
54063
  if (options.health === false) return void 0;
@@ -53668,8 +54209,8 @@ function withoutRoot(record2) {
53668
54209
  return rest;
53669
54210
  }
53670
54211
  function normalizeMemberPath(p) {
53671
- const posix6 = path64.posix.normalize(p.replace(/\\/g, "/"));
53672
- return posix6.replace(/^\.\/+/, "").replace(/\/+$/, "");
54212
+ const posix8 = path64.posix.normalize(p.replace(/\\/g, "/"));
54213
+ return posix8.replace(/^\.\/+/, "").replace(/\/+$/, "");
53673
54214
  }
53674
54215
  function projectRoot(dataDir, id) {
53675
54216
  return path64.join(dataDir, "projects", id);
@@ -57374,12 +57915,13 @@ function mintKey(cfg, credential, project2, role) {
57374
57915
  "instance-wide super-admin (*:*) keys cannot be minted \u2014 the built-in admin account (WAIRON_ADMIN_USER) is the only super-admin"
57375
57916
  );
57376
57917
  }
57918
+ const narrowed2 = assertMintableNarrowingEntry(cfg.dataDir, project2);
57377
57919
  const token = "wk_" + crypto17.randomBytes(24).toString("hex");
57378
57920
  const record2 = {
57379
57921
  id: crypto17.randomBytes(6).toString("hex"),
57380
57922
  keyHash: hashToken(token),
57381
57923
  role,
57382
- projects: project2 === "*" ? ["*"] : [project2],
57924
+ projects: [narrowed2],
57383
57925
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
57384
57926
  };
57385
57927
  createCredential(cfg.dataDir, record2);
@@ -57472,7 +58014,7 @@ function refuseDesignErrors(gate) {
57472
58014
  function refuseUnapprovedMembers(required, members) {
57473
58015
  const unapproved = members.filter((m) => m.as !== "part" && m.state !== "approved");
57474
58016
  if (!required || unapproved.length === 0) return;
57475
- const named2 = unapproved.map((m) => `${m.alias ?? m.key} (${m.state}${m.upgraded ? " \u2014 approved under the pre-stage-5 identity, re-lock it once" : ""})`);
58017
+ const named2 = unapproved.map((m) => `${m.alias ?? m.key} (${m.state}${m.upgraded ? " \u2014 approved under an earlier gate identity, re-lock it once" : ""})`);
57476
58018
  throw new LockRefusedError2(
57477
58019
  `composition.requireApprovedMembers: direct member(s) not approved \u2014 ${named2.join(", ")}. Lock each at its own root first; a parent never approves below itself. Nothing was written.`
57478
58020
  );
@@ -61654,7 +62196,7 @@ async function awaitApproval(cfg, credential, requestId, timeoutSeconds) {
61654
62196
  let current2 = req;
61655
62197
  while (current2.status === "pending" && Date.now() < deadline) {
61656
62198
  const remaining = deadline - Date.now();
61657
- await new Promise((resolve55) => setTimeout(resolve55, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
62199
+ await new Promise((resolve56) => setTimeout(resolve56, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
61658
62200
  expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
61659
62201
  current2 = getApprovalRequestById(cfg.dataDir, requestId) ?? current2;
61660
62202
  }
@@ -67253,8 +67795,9 @@ function stop() {
67253
67795
  var DEV_UNIT_ID = "local";
67254
67796
  function bootstrapInstance(config) {
67255
67797
  ensureInstanceIdentity(config.dataDir);
67256
- if (config.devMode && !getOrganizationUnit(config.dataDir, DEV_UNIT_ID)) {
67257
- const system = { userId: "system", kind: "service", issuer: "local" };
67798
+ if (!config.devMode) return;
67799
+ const system = { userId: "system", kind: "service", issuer: "local" };
67800
+ if (!getOrganizationUnit(config.dataDir, DEV_UNIT_ID)) {
67258
67801
  createUnit(config.dataDir, {
67259
67802
  id: DEV_UNIT_ID,
67260
67803
  name: "Local Development",
@@ -67266,6 +67809,18 @@ function bootstrapInstance(config) {
67266
67809
  createdBy: system
67267
67810
  });
67268
67811
  }
67812
+ const placed = new Set(listProjectPlacements(config.dataDir).map((p) => p.projectId));
67813
+ for (const record2 of listProjectRecords(config.dataDir)) {
67814
+ if (placed.has(record2.id) || record2.parentProjectId !== void 0) continue;
67815
+ placeProject(config.dataDir, {
67816
+ id: "",
67817
+ projectId: record2.id,
67818
+ unitId: DEV_UNIT_ID,
67819
+ role: "owner",
67820
+ createdAt: "",
67821
+ createdBy: system
67822
+ });
67823
+ }
67269
67824
  }
67270
67825
 
67271
67826
  // src/server/identity-portal.ts
@@ -68602,11 +69157,11 @@ async function runServe(options = {}) {
68602
69157
  logger.info(` data dir: ${import_chalk18.default.gray(cfg.dataDir)}`);
68603
69158
  logger.blank();
68604
69159
  logger.info("Press Ctrl+C to stop.");
68605
- await new Promise((resolve55) => {
69160
+ await new Promise((resolve56) => {
68606
69161
  const shutdown = () => {
68607
69162
  logger.info("Shutting down\u2026");
68608
69163
  handle.close();
68609
- resolve55();
69164
+ resolve56();
68610
69165
  };
68611
69166
  process.on("SIGINT", shutdown);
68612
69167
  process.on("SIGTERM", shutdown);
@@ -68671,11 +69226,11 @@ async function runDev(options = {}) {
68671
69226
  logger.blank();
68672
69227
  logger.info("An agent edits specs; refresh the page to see the live graph. Press Ctrl+C to stop.");
68673
69228
  if (options.open) openBrowser(url);
68674
- await new Promise((resolve55) => {
69229
+ await new Promise((resolve56) => {
68675
69230
  const shutdown = () => {
68676
69231
  logger.info("Shutting down\u2026");
68677
69232
  handle.close();
68678
- resolve55();
69233
+ resolve56();
68679
69234
  };
68680
69235
  process.on("SIGINT", shutdown);
68681
69236
  process.on("SIGTERM", shutdown);
@@ -68781,7 +69336,9 @@ async function runHostUnit(action, options = {}) {
68781
69336
  id: qualifiedId2,
68782
69337
  name: options.name ?? options.slug,
68783
69338
  slug: options.slug,
68784
- kind: options.kind ?? "team",
69339
+ // A top-level unit must be a business_entity (the org-unit hierarchy's
69340
+ // root kind), so that is the default without --parent; under a parent, team.
69341
+ kind: options.kind ?? (options.parent ? "team" : "business_entity"),
68785
69342
  ...options.parent ? { parentId: options.parent } : {},
68786
69343
  status: "active",
68787
69344
  createdAt: "",
@@ -68870,6 +69427,21 @@ function resolveScopeOptions(options) {
68870
69427
  if (options.unit) return { kind: "unit", id: options.unit };
68871
69428
  return { kind: "instance" };
68872
69429
  }
69430
+ function warnIfUnreadable(cfg, cred, userId, scope, why) {
69431
+ const target = scope.kind === "subsystem" ? { kind: "project", id: (scope.id ?? "").split("/")[0] } : scope;
69432
+ let read2;
69433
+ try {
69434
+ read2 = explain3(cfg, cred, userId, "project:read", target.kind, target.id ?? "").value;
69435
+ } catch {
69436
+ return;
69437
+ }
69438
+ if (read2 === "yes") return;
69439
+ const where = target.kind === "instance" ? "the instance" : `${target.kind} ${target.id}`;
69440
+ const flag = target.kind === "instance" ? "--instance" : `--${target.kind} ${target.id}`;
69441
+ logger.warn(
69442
+ `project:read for "${userId}" at ${where} resolves to ${read2} \u2014 ${why}. Grant it with \`wairon host permission set --user ${userId} --capability project:read ${flag}\`.`
69443
+ );
69444
+ }
68873
69445
  async function runHostPermission(action, options = {}) {
68874
69446
  const cfg = resolveHostConfig(options);
68875
69447
  const cred = masterCredential();
@@ -68898,6 +69470,9 @@ async function runHostPermission(action, options = {}) {
68898
69470
  logger.success(
68899
69471
  `Assignment ${stored.id}: ${options.user} ${options.capability}=${value} @ ${scope.kind}${scope.id ? ` ${scope.id}` : ""}`
68900
69472
  );
69473
+ if (options.capability === "project:write" && (value === "yes" || value === "approval")) {
69474
+ warnIfUnreadable(cfg, cred, options.user, scope, `${options.user} can write here but cannot read it`);
69475
+ }
68901
69476
  break;
68902
69477
  }
68903
69478
  case "list": {
@@ -68951,7 +69526,16 @@ async function runHostKey(action, options = {}) {
68951
69526
  console.log(` ${import_chalk18.default.bold(minted.token)}`);
68952
69527
  logger.blank();
68953
69528
  for (const line2 of minted.mapped) {
68954
- logger.warn(`Narrowing ${line2}: a member is a record of its own since stage 7, so the token names it by its id (qualified entries are deprecated).`);
69529
+ logger.warn(`Narrowing ${line2}: a member is a record of its own, so the token names it by its id (qualified entries are deprecated).`);
69530
+ }
69531
+ if (options.project !== "*") {
69532
+ warnIfUnreadable(
69533
+ cfg,
69534
+ cred,
69535
+ options.owner,
69536
+ { kind: "project", id: options.project },
69537
+ `the token acts as "${options.owner}"'s live permission, so it cannot read the project either`
69538
+ );
68955
69539
  }
68956
69540
  break;
68957
69541
  }
@@ -69355,14 +69939,23 @@ async function runExport(out) {
69355
69939
  }
69356
69940
  const approval = verdict.state;
69357
69941
  const design = exportDesign3(out, approval);
69942
+ const members = memberNote(design);
69358
69943
  if (out) {
69359
69944
  logger.success(
69360
69945
  `Design export of "${design.project.name}" written to ${path92.resolve(out)} (${design.components.length} component(s), ${design.types.length} type(s); approval: ${design.source.approval}).`
69361
69946
  );
69947
+ if (members) logger.info(members);
69362
69948
  return;
69363
69949
  }
69364
69950
  process.stdout.write(`${JSON.stringify(design, null, 2)}
69365
69951
  `);
69952
+ if (members && design.components.length === 0 && design.types.length === 0) process.stderr.write(`${members}
69953
+ `);
69954
+ }
69955
+ function memberNote(design) {
69956
+ const members = design.dependencies.filter((d) => d.role === "member").map((d) => d.alias);
69957
+ if (members.length === 0) return null;
69958
+ return `${members.length} member project(s) are listed under \`dependencies\`, not inlined (${members.join(", ")}): run \`wairon export\` in a member's own root for its design.`;
69366
69959
  }
69367
69960
 
69368
69961
  // src/commands/externals.ts
@@ -69585,16 +70178,16 @@ program.command("generate").description("Generate agent output files from the sp
69585
70178
  dryRun: opts.dryRun
69586
70179
  });
69587
70180
  });
69588
- program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against the committed .wai/lock.json. Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
70181
+ program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against .wai/lock.json as it is in the working tree (in CI, the file committed in the checked-out revision; locally, an uncommitted record counts too). Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
69589
70182
  await lockCheckCommand(opts);
69590
70183
  });
69591
- program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
70184
+ program.command("lock").description("Record human approval of the design: validate the spec tree as complete, write the approval to .wai/lock.json (one digest per spec; spec files are not rewritten), and refresh this project's generated outputs \u2014 only if the design validates. Commit .wai/lock.json; `wairon lock-check` gates on it. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "accepted for one release and ignored: a lock approves this project only; each member locks at its own root").action(async (opts) => {
69592
70185
  await lockCommand(opts);
69593
70186
  });
69594
- program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines (notices are printed and counted, never fatal)").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "at a project that declares members: run the owner's gate alone instead of the family run").option("--family", "run the family run from here: every member's own gate, and this project's externals composed against their live producers").action(async (opts) => {
70187
+ program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines (notices are printed and counted, never fatal)").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "at a project that declares members: run the owner's gate alone instead of the family run").option("--family", "run the family run from here: every member's own gate, and this project's externals composed against their live producers").option("--all", "print every finding (by default the first 100 per severity are printed, followed by per-code totals)").action(async (opts) => {
69595
70188
  await validateCommand(opts);
69596
70189
  });
69597
- program.command("status").description("Show a hierarchical completeness graph of the SDD Spec Tree").option("--subsystem <id>", "only show status for the specified subsystem").option("--no-recursive", "do not recursively show status for subprojects").action(async (opts) => {
70190
+ program.command("status").description("Show the spec tree with its authoring readiness (how far each spec is written out, from its draft/design/complete status) and, separately, its approval state from .wai/lock.json").option("--subsystem <id>", "only show status for the specified subsystem").option("--no-recursive", "show this project only, without its members").action(async (opts) => {
69598
70191
  await statusCommand(opts);
69599
70192
  });
69600
70193
  program.command("doctor").description("Health check: flags stale generated guides/skills, an unregistered MCP server, spec-tree issues and the chaining migration still pending").option("--fix", "regenerate stale in-project guides/context/skills, register the MCP server, then apply the chaining migration once confirmed").option("--report <section>", "print one section's report and nothing else, writing nothing (chaining | composed-validation); never combines with --fix").option("-y, --yes", "answer the chaining migration's confirmation (for a non-interactive --fix); a write outside the project root also needs --global").option("--global", "consent to the --fix writes outside the project root (a machine-wide MCP config, the legacy global plugin); with --yes it answers their confirmation, and each replaced file is backed up beside itself").action(async (opts) => {
@@ -69774,7 +70367,7 @@ hostCmd.command("project <action>").description("create | list | destroy a hoste
69774
70367
  hostCmd.command("demo").description("provision a project seeded with a rich example spec tree, so the canvas has content to render").option("--id <id>", "project id", "demo").option("--unit <unitId>", "owner unit id (created if absent)", "demo").option("--force", "destroy and reseed the project if it already exists").option("--data-dir <path>", "data root (default WAIRON_DATA_DIR or ~/.wairon/data)").action(async (opts) => {
69775
70368
  await runHostDemo({ id: opts.id, unit: opts.unit, force: opts.force, dataDir: opts.dataDir });
69776
70369
  });
69777
- hostCmd.command("unit <action>").description("create an organization unit (projects are placed into units at creation)").option("--slug <slug>", "unit slug (REQUIRED for create; a root unit's id IS its slug)").option("--name <name>", "display name (defaults to the slug)").option("--kind <kind>", "unit kind (business_entity | department | team | \u2026)", "team").option("--parent <unitId>", "qualified id of the parent unit (omit for a root unit)").option("--data-dir <path>", "data root").action(async (action, opts) => {
70370
+ hostCmd.command("unit <action>").description("create an organization unit (projects are placed into units at creation)").option("--slug <slug>", "unit slug (REQUIRED for create; a root unit's id IS its slug)").option("--name <name>", "display name (defaults to the slug)").option("--kind <kind>", "unit kind (business_entity | department | team | \u2026); default business_entity for a top-level unit, team under --parent").option("--parent <unitId>", "qualified id of the parent unit (omit for a root unit)").option("--data-dir <path>", "data root").action(async (action, opts) => {
69778
70371
  await runHostUnit(action, { slug: opts.slug, name: opts.name, kind: opts.kind, parent: opts.parent, dataDir: opts.dataDir });
69779
70372
  });
69780
70373
  hostCmd.command("doctor").description("migrate a hosted data dir to the permission model (grants \u2192 assignments, owners, unit ids, placements); dry-run without --fix").option("--fix", "apply the migration (REQUIRED at rollout \u2014 legacy users/tokens otherwise resolve to zero permissions)").option("--data-dir <path>", "data root").action(async (opts) => {