@wairon/cli 5.1.1-dev.88 → 5.1.1-dev.89

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.88";
68
+ WAIRON_VERSION = "5.1.1-dev.89";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -201,6 +201,7 @@ var fs_exports = {};
201
201
  __export(fs_exports, {
202
202
  aiDir: () => aiDir,
203
203
  aiDirAt: () => aiDirAt,
204
+ backupBeside: () => backupBeside,
204
205
  currentRootBinding: () => currentRootBinding,
205
206
  ensureDir: () => ensureDir,
206
207
  findProjectRoot: () => findProjectRoot,
@@ -211,6 +212,7 @@ __export(fs_exports, {
211
212
  getProjectRootOverride: () => getProjectRootOverride,
212
213
  getRequestParentReach: () => getRequestParentReach,
213
214
  getRequestProjectRoot: () => getRequestProjectRoot,
215
+ isOutsideRoot: () => isOutsideRoot,
214
216
  listFiles: () => listFiles,
215
217
  listFilesRecursive: () => listFilesRecursive,
216
218
  pathExists: () => pathExists,
@@ -248,6 +250,15 @@ function readFileOrNull(filePath) {
248
250
  function pathExists(targetPath) {
249
251
  return fs3.existsSync(targetPath);
250
252
  }
253
+ function isOutsideRoot(root, target) {
254
+ const rel2 = path4.relative(path4.resolve(root), path4.resolve(target));
255
+ return rel2 === ".." || rel2.startsWith(`..${path4.sep}`) || path4.isAbsolute(rel2);
256
+ }
257
+ function backupBeside(file, at = /* @__PURE__ */ new Date()) {
258
+ const backup = `${file}.wairon-backup-${at.toISOString().replace(/[:.]/g, "-")}`;
259
+ fs3.copyFileSync(file, backup);
260
+ return backup;
261
+ }
251
262
  function listFiles(dirPath, ext) {
252
263
  if (!fs3.existsSync(dirPath)) return [];
253
264
  return fs3.readdirSync(dirPath).filter((f) => f.endsWith(ext)).map((f) => path4.join(dirPath, f));
@@ -2831,6 +2842,12 @@ function parseDeclaredCall(ref) {
2831
2842
  if (at <= 0 || at === ref.length - 1) return null;
2832
2843
  return { compId: ref.slice(0, at), methodName: ref.slice(at + 1) };
2833
2844
  }
2845
+ function technologyName(tech) {
2846
+ return typeof tech === "string" ? tech : tech.name;
2847
+ }
2848
+ function technologyTokens(tech) {
2849
+ return typeof tech === "string" ? [tech] : [...tech.matches];
2850
+ }
2834
2851
  function methodSourceFile(method2, implementationSourcePath) {
2835
2852
  return method2.sourcePath || implementationSourcePath || void 0;
2836
2853
  }
@@ -2890,7 +2907,7 @@ function typeSourceFiles(type) {
2890
2907
  for (const method2 of type.methods ?? []) add2(method2.sourcePath);
2891
2908
  return files;
2892
2909
  }
2893
- var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceTypeExportSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, ExternalLockEntrySchema, ExternalsLockSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
2910
+ var import_zod8, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, RETIRED_STEREOTYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, DependencyClassSchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, PortalMountSchema, ComponentSpecSchema, BLOCK_NOUNS, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, FindingDeclarationSchema, MethodSignatureSchema, INTENT_FLOOR_MIN_CHARS, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, TechnologySchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceTypeExportSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, ExternalLockEntrySchema, ExternalsLockSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
2894
2911
  var init_specs = __esm({
2895
2912
  "src/models/specs.ts"() {
2896
2913
  "use strict";
@@ -3564,6 +3581,13 @@ var init_specs = __esm({
3564
3581
  /** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
3565
3582
  ext: ExtDataSchema.optional()
3566
3583
  });
3584
+ TechnologySchema = import_zod8.z.union([
3585
+ import_zod8.z.string().min(1),
3586
+ import_zod8.z.object({
3587
+ name: import_zod8.z.string().min(1),
3588
+ matches: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
3589
+ }).strict()
3590
+ ]);
3567
3591
  ImplementationSpecSchema = import_zod8.z.object({
3568
3592
  id: SpecIdSchema,
3569
3593
  name: import_zod8.z.string(),
@@ -3588,9 +3612,10 @@ var init_specs = __esm({
3588
3612
  * component's ownership tree the technology's home: references anywhere
3589
3613
  * outside it are flagged (TECH_LEAKAGE), contract identifiers must stay
3590
3614
  * intent-language (VENDOR_NAME_IN_CONTRACT), and only data-layer
3591
- * stereotypes should bind tech directly (TECH_ON_LOGIC_COMPONENT).
3615
+ * stereotypes should bind tech directly (TECH_ON_LOGIC_COMPONENT). An entry
3616
+ * is a bare name or `{ name, matches }` (see TechnologySchema).
3592
3617
  */
3593
- technologies: import_zod8.z.array(import_zod8.z.string()).optional(),
3618
+ technologies: import_zod8.z.array(TechnologySchema).optional(),
3594
3619
  /**
3595
3620
  * The parameter names THIS realization takes BEFORE the ones its contract
3596
3621
  * declares — a config object, a data root, the transport handles a portal is
@@ -19143,6 +19168,7 @@ var DATA_LAYER, technologyBindingRule;
19143
19168
  var init_technology_binding = __esm({
19144
19169
  "src/core/rules/heuristic/technology-binding.ts"() {
19145
19170
  "use strict";
19171
+ init_models();
19146
19172
  DATA_LAYER = /* @__PURE__ */ new Set(["Adapter", "Store", "Registry", "Index"]);
19147
19173
  technologyBindingRule = {
19148
19174
  name: "technology-binding",
@@ -19159,7 +19185,7 @@ var init_technology_binding = __esm({
19159
19185
  ctx.addIssue(
19160
19186
  "warning",
19161
19187
  "TECH_ON_LOGIC_COMPONENT",
19162
- `Implementation "${impl.id}" binds technology (${impl.technologies.join(", ")}) on component "${comp.id}" (${comp.componentType}). Technology belongs behind a data-layer seam \u2014 extract an Adapter (or Store/Registry/Index) behind an intent interface and let this component depend on that.`,
19188
+ `Implementation "${impl.id}" binds technology (${impl.technologies.map(technologyName).join(", ")}) on component "${comp.id}" (${comp.componentType}). Technology belongs behind a data-layer seam \u2014 extract an Adapter (or Store/Registry/Index) behind an intent interface and let this component depend on that.`,
19163
19189
  impl.id,
19164
19190
  ctx.isImplementationDraft(impl)
19165
19191
  );
@@ -19186,6 +19212,16 @@ function makeMatcher(tech) {
19186
19212
  return false;
19187
19213
  };
19188
19214
  }
19215
+ function newHome(label7) {
19216
+ const home = {
19217
+ label: label7,
19218
+ matchers: [],
19219
+ match: (text3) => home.matchers.some((m) => m(text3)),
19220
+ ownerComponents: /* @__PURE__ */ new Set(),
19221
+ scope: /* @__PURE__ */ new Set()
19222
+ };
19223
+ return home;
19224
+ }
19189
19225
  function contractIdentifiers(m) {
19190
19226
  const parts = [m.name, m.signature, m.returns];
19191
19227
  for (const p of m.params ?? []) parts.push(p.name, p.type);
@@ -19212,7 +19248,7 @@ var init_technology_boundaries = __esm({
19212
19248
  technologyRule = {
19213
19249
  name: "technology-boundaries",
19214
19250
  judges: "design",
19215
- description: "Technology stays behind its owning boundary: an L4 that declares `technologies` (e.g. [mysql]) makes its component's ownership tree the technology's home. References outside that tree are leakage, and L3 contract identifiers must stay intent-language \u2014 the contract is the swap seam, so the vendor name is wrong even on the owning component's own interface.",
19251
+ description: "Technology stays behind its owning boundary: an L4 that declares `technologies` (e.g. [mysql]) makes its component's ownership tree the technology's home. References outside that tree are leakage, and L3 contract identifiers must stay intent-language \u2014 the contract is the swap seam, so the vendor name is wrong even on the owning component's own interface. A technology is matched by its name, or \u2014 when it declares `matches` because its name is also an ordinary word of the tree (a package named after the file format it reads) \u2014 by those tokens alone.",
19216
19252
  codes: [
19217
19253
  { code: "TECH_LEAKAGE", defaultSeverity: "warning", summary: "Technology referenced outside its owning boundary" },
19218
19254
  { code: "VENDOR_NAME_IN_CONTRACT", defaultSeverity: "warning", summary: "Technology name in L3 contract identifiers" }
@@ -19249,7 +19285,9 @@ var init_technology_boundaries = __esm({
19249
19285
  const intf = ctx.interfaceMap.get(impl.contract);
19250
19286
  const comp = intf ? ctx.componentMap.get(intf.component) : void 0;
19251
19287
  if (!comp) continue;
19252
- for (const tech of impl.technologies) declarations.push({ comp, tech });
19288
+ for (const tech of impl.technologies) {
19289
+ declarations.push({ comp, name: technologyName(tech), tokens: technologyTokens(tech) });
19290
+ }
19253
19291
  }
19254
19292
  const scopes = /* @__PURE__ */ new Map();
19255
19293
  const scopeOf3 = (comp) => {
@@ -19273,11 +19311,12 @@ var init_technology_boundaries = __esm({
19273
19311
  return scope;
19274
19312
  };
19275
19313
  const homes = /* @__PURE__ */ new Map();
19276
- for (const { comp, tech } of declarations) {
19277
- const match2 = makeMatcher(tech);
19278
- if (!match2) continue;
19279
- const key = tech.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("");
19280
- const home = homes.get(key) ?? { label: tech, match: match2, ownerComponents: /* @__PURE__ */ new Set(), scope: /* @__PURE__ */ new Set() };
19314
+ for (const { comp, name, tokens } of declarations) {
19315
+ const matchers = tokens.map(makeMatcher).filter((m) => m !== null);
19316
+ if (!matchers.length) continue;
19317
+ const key = name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("");
19318
+ const home = homes.get(key) ?? newHome(name);
19319
+ home.matchers.push(...matchers);
19281
19320
  home.ownerComponents.add(comp.id);
19282
19321
  for (const id of scopeOf3(comp)) home.scope.add(id);
19283
19322
  homes.set(key, home);
@@ -22936,8 +22975,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
22936
22975
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
22937
22976
  for (const step of steps) {
22938
22977
  for (const par of parallelArms) {
22939
- const sep16 = par.sepByStep.get(step.stepNumber);
22940
- if (sep16 !== void 0) lines.push(` and ${escapeLabel(sep16)}`);
22978
+ const sep17 = par.sepByStep.get(step.stepNumber);
22979
+ if (sep17 !== void 0) lines.push(` and ${escapeLabel(sep17)}`);
22941
22980
  }
22942
22981
  switch (step.type) {
22943
22982
  case "local":
@@ -24046,7 +24085,7 @@ function mapSpecReferences(kind, spec, map) {
24046
24085
  const s = spec;
24047
24086
  return {
24048
24087
  ...s,
24049
- publicInterfaces: s.publicInterfaces.map((p) => ({
24088
+ publicInterfaces: (s.publicInterfaces ?? []).map((p) => ({
24050
24089
  ...p,
24051
24090
  ...p.component !== void 0 ? { component: map("publicInterfaces", p.component) } : {},
24052
24091
  ...p.interface !== void 0 ? { interface: map("publicInterfaces", p.interface) } : {},
@@ -24062,8 +24101,8 @@ function mapSpecReferences(kind, spec, map) {
24062
24101
  return {
24063
24102
  ...c,
24064
24103
  subsystem: map("subsystem", c.subsystem),
24065
- owns: c.owns.map((o) => map("owns", o)),
24066
- dependsOn: c.dependsOn.map((d) => map("dependsOn", d)),
24104
+ owns: (c.owns ?? []).map((o) => map("owns", o)),
24105
+ dependsOn: (c.dependsOn ?? []).map((d) => map("dependsOn", d)),
24067
24106
  ...c.dispatch ? { dispatch: c.dispatch.map((b) => ({ ...b, component: map("dispatch", b.component) })) } : {},
24068
24107
  ...c.mounts ? { mounts: c.mounts.map((m) => ({ ...m, portal: map("mounts", m.portal) })) } : {}
24069
24108
  };
@@ -24077,9 +24116,9 @@ function mapSpecReferences(kind, spec, map) {
24077
24116
  return {
24078
24117
  ...impl,
24079
24118
  contract: map("contract", impl.contract),
24080
- methods: impl.methods.map((m) => ({
24119
+ methods: (impl.methods ?? []).map((m) => ({
24081
24120
  ...m,
24082
- narrative: m.narrative.map((step) => step.targetComponent !== void 0 ? { ...step, targetComponent: map("narrative", step.targetComponent) } : step)
24121
+ narrative: (m.narrative ?? []).map((step) => step.targetComponent !== void 0 ? { ...step, targetComponent: map("narrative", step.targetComponent) } : step)
24083
24122
  }))
24084
24123
  };
24085
24124
  }
@@ -25459,7 +25498,7 @@ function rewriteReferences(kind, id, edits) {
25459
25498
  function moveMethods(from, to, methods, hooks, dryRun) {
25460
25499
  return current().moveMethods(from, to, methods, hooks, dryRun);
25461
25500
  }
25462
- var fs14, path21, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, workspaces, lastServedBinding;
25501
+ var fs14, path21, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
25463
25502
  var init_specs2 = __esm({
25464
25503
  "src/core/specs.ts"() {
25465
25504
  "use strict";
@@ -26332,10 +26371,10 @@ var init_specs2 = __esm({
26332
26371
  return this.relativizeSpec("interface", spec);
26333
26372
  }
26334
26373
  prepareImplementationForWrite(spec, carry = true) {
26335
- const relative25 = this.relativizeSpec("implementation", spec, carry);
26374
+ const relative26 = this.relativizeSpec("implementation", spec, carry);
26336
26375
  const member = this.getSubprojectPrefix(spec.id);
26337
26376
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
26338
- return memberRoot ? childRelativeFilePaths(relative25, this.rootDir, memberRoot) : relative25;
26377
+ return memberRoot ? childRelativeFilePaths(relative26, this.rootDir, memberRoot) : relative26;
26339
26378
  }
26340
26379
  prepareTypeForWrite(spec) {
26341
26380
  return this.relativizeSpec("type", spec);
@@ -27174,7 +27213,10 @@ var init_specs2 = __esm({
27174
27213
  * second delete written as 7 addresses what used to be step 8. That is why
27175
27214
  * a delete may restate the step's `label` or `description` as a guard, and
27176
27215
  * why labels are the reliable way to name a jump target. Inserts and
27177
- * deletes relocate every jump field in the same narrative around them; a
27216
+ * deletes relocate every STORED jump field in the same narrative around
27217
+ * them; a jump the delta itself writes is already in the numbering the
27218
+ * whole delta leaves behind, so none of its later steps moves it. A new
27219
+ * implementation method with no narrative is intent-level ([]); a
27178
27220
  * step whose `type` changes is REBUILT for its new type, keeping its
27179
27221
  * description and label.
27180
27222
  * 5. UNSET the fields the delta names, at the level each `unset` sits in.
@@ -27232,16 +27274,19 @@ var init_specs2 = __esm({
27232
27274
  }
27233
27275
  const JUMP_FIELDS = ["onTrueStep", "onFalseStep", "defaultStep", "endStep", "finallyStep", "toStep"];
27234
27276
  const JUMP_LIST_FIELDS = ["cases", "catches", "branches"];
27277
+ const isPinned = (step, field) => step?.[DELTA_JUMP_PINS] instanceof Set && step[DELTA_JUMP_PINS].has(field);
27278
+ const isPinnedEntry = (entry) => entry?.[DELTA_JUMP_PINS] === true;
27235
27279
  const relocateJumps = (step, shiftFrom, deltaN, captureInsertTarget = false) => {
27236
27280
  const hit = (v, isRegionTail = false) => deltaN > 0 ? captureInsertTarget && !isRegionTail ? v > shiftFrom : v >= shiftFrom : v > shiftFrom;
27237
27281
  const out = { ...step };
27238
27282
  for (const f of JUMP_FIELDS) {
27283
+ if (isPinned(step, f)) continue;
27239
27284
  if (typeof out[f] === "number" && hit(out[f], f === "endStep")) out[f] = out[f] + deltaN;
27240
27285
  }
27241
27286
  for (const lf of JUMP_LIST_FIELDS) {
27242
27287
  if (Array.isArray(out[lf])) {
27243
27288
  out[lf] = out[lf].map(
27244
- (c) => typeof c?.step === "number" && hit(c.step) ? { ...c, step: c.step + deltaN } : c
27289
+ (c) => !isPinnedEntry(c) && typeof c?.step === "number" && hit(c.step) ? { ...c, step: c.step + deltaN } : c
27245
27290
  );
27246
27291
  }
27247
27292
  }
@@ -27249,14 +27294,39 @@ var init_specs2 = __esm({
27249
27294
  };
27250
27295
  const jumpRefsTo = (step, target) => {
27251
27296
  const refs = [];
27252
- for (const f of JUMP_FIELDS) if (step[f] === target) refs.push(f);
27297
+ for (const f of JUMP_FIELDS) if (step[f] === target && !isPinned(step, f)) refs.push(f);
27253
27298
  for (const lf of JUMP_LIST_FIELDS) {
27254
27299
  (Array.isArray(step[lf]) ? step[lf] : []).forEach((c, i) => {
27255
- if (c?.step === target) refs.push(`${lf}[${i}].step`);
27300
+ if (c?.step === target && !isPinnedEntry(c)) refs.push(`${lf}[${i}].step`);
27256
27301
  });
27257
27302
  }
27258
27303
  return refs;
27259
27304
  };
27305
+ const pinDeltaJumps = (deltaStep, carried) => {
27306
+ const pins = new Set(carried ?? []);
27307
+ for (const f of JUMP_FIELDS) if (typeof deltaStep[f] === "number") pins.add(f);
27308
+ const out = { ...deltaStep };
27309
+ for (const lf of JUMP_LIST_FIELDS) {
27310
+ if (!Array.isArray(out[lf])) continue;
27311
+ out[lf] = out[lf].map((c) => c && typeof c === "object" && typeof c.step === "number" ? { ...c, [DELTA_JUMP_PINS]: true } : c);
27312
+ }
27313
+ out[DELTA_JUMP_PINS] = pins;
27314
+ return out;
27315
+ };
27316
+ const unpinned = (step) => {
27317
+ const out = { ...step };
27318
+ delete out[DELTA_JUMP_PINS];
27319
+ for (const lf of JUMP_LIST_FIELDS) {
27320
+ if (!Array.isArray(out[lf])) continue;
27321
+ out[lf] = out[lf].map((c) => {
27322
+ if (!c || typeof c !== "object" || !(DELTA_JUMP_PINS in c)) return c;
27323
+ const entry = { ...c };
27324
+ delete entry[DELTA_JUMP_PINS];
27325
+ return entry;
27326
+ });
27327
+ }
27328
+ return out;
27329
+ };
27260
27330
  const normalizeIdentity = (k) => String(k ?? "").toLowerCase().replace(/[_\-\s]/g, "");
27261
27331
  const unsetFieldsOf = (item) => Array.isArray(item?.unset) ? item.unset.filter((f) => typeof f === "string") : [];
27262
27332
  const withUnset = (merged, deltaItem) => {
@@ -27436,21 +27506,24 @@ var init_specs2 = __esm({
27436
27506
  capture
27437
27507
  ));
27438
27508
  const { action, remove: remove3, captureJumps, ...cleanStep } = deltaStep;
27439
- steps.push(withUnset(cleanStep, deltaStep));
27509
+ steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
27440
27510
  } else {
27441
27511
  const idx = steps.findIndex((s) => s.stepNumber === stepNum);
27442
27512
  if (idx !== -1) {
27443
- const { action, remove: remove3, ...cleanStep } = deltaStep;
27513
+ const { action, remove: remove3, ...rawStep } = deltaStep;
27444
27514
  const stored = steps[idx];
27515
+ const cleanStep = pinDeltaJumps(rawStep, stored[DELTA_JUMP_PINS]);
27445
27516
  const merged = withUnset(retargetByLabel(mergeElement(stored, cleanStep), cleanStep), deltaStep);
27446
- steps[idx] = rebuildOnRetype(stored, cleanStep, merged, methodName);
27517
+ const rebuilt = rebuildOnRetype(stored, rawStep, merged, methodName);
27518
+ rebuilt[DELTA_JUMP_PINS] = cleanStep[DELTA_JUMP_PINS];
27519
+ steps[idx] = rebuilt;
27447
27520
  } else {
27448
27521
  const { action, remove: remove3, ...cleanStep } = deltaStep;
27449
- steps.push(withUnset(cleanStep, deltaStep));
27522
+ steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
27450
27523
  }
27451
27524
  }
27452
27525
  }
27453
- return steps.sort((a, b) => a.stepNumber - b.stepNumber);
27526
+ return steps.map(unpinned).sort((a, b) => a.stepNumber - b.stepNumber);
27454
27527
  };
27455
27528
  const isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
27456
27529
  const mergeExt = (existing, delta2) => {
@@ -27473,6 +27546,11 @@ var init_specs2 = __esm({
27473
27546
  } else {
27474
27547
  const existingMethod = merged[idx];
27475
27548
  let narrative = existingMethod.narrative ? [...existingMethod.narrative] : [];
27549
+ if (deltaMethod.narrative !== void 0 && deltaMethod.narrative !== null && !Array.isArray(deltaMethod.narrative)) {
27550
+ throw new Error(
27551
+ `Refusing to update implementation method "${String(deltaMethod.name)}": its "narrative" must be a list of step deltas, got ${JSON.stringify(deltaMethod.narrative)}.`
27552
+ );
27553
+ }
27476
27554
  if (Array.isArray(deltaMethod.narrative)) {
27477
27555
  narrative = deltaMethod.narrative.length === 0 ? [] : mergeNarrative(String(deltaMethod.name), narrative, deltaMethod.narrative);
27478
27556
  }
@@ -27485,11 +27563,21 @@ var init_specs2 = __esm({
27485
27563
  }
27486
27564
  } else {
27487
27565
  assertUnmatchedIsAnAddition(deltaMethod, deltaMethod.name, merged.map((m) => m.name), "method");
27488
- merged.push(withUnset(deltaMethod, deltaMethod));
27566
+ merged.push(withUnset(newMethodOf(deltaMethod), deltaMethod));
27489
27567
  }
27490
27568
  }
27491
27569
  return merged;
27492
27570
  };
27571
+ const newMethodOf = (deltaMethod) => {
27572
+ if (kind !== "implementation") return deltaMethod;
27573
+ if (deltaMethod.narrative === void 0 || deltaMethod.narrative === null) return { ...deltaMethod, narrative: [] };
27574
+ if (!Array.isArray(deltaMethod.narrative)) {
27575
+ throw new Error(
27576
+ `Refusing to add implementation method "${String(deltaMethod.name)}": its "narrative" must be a list of steps, got ${JSON.stringify(deltaMethod.narrative)}. Leave it out (or pass []) for an intent-level method.`
27577
+ );
27578
+ }
27579
+ return deltaMethod;
27580
+ };
27493
27581
  const mergeNamedArray = (existing, delta2) => {
27494
27582
  const merged = [...existing];
27495
27583
  for (const deltaItem of delta2) {
@@ -27667,6 +27755,12 @@ var init_specs2 = __esm({
27667
27755
  const qualifiedDelta = qualifyDeltaRefs(mergeableDelta);
27668
27756
  const mergedResult = mergeDelta(result, qualifiedDelta);
27669
27757
  for (const field of unsetFields) delete mergedResult[field];
27758
+ const withDefaults = deltaSchema.safeParse(mergedResult);
27759
+ if (withDefaults.success) {
27760
+ for (const [key, value] of Object.entries(withDefaults.data)) {
27761
+ if (!(key in mergedResult)) mergedResult[key] = value;
27762
+ }
27763
+ }
27670
27764
  if (kind === "implementation" && Array.isArray(mergedResult.methods)) {
27671
27765
  const labelErrors = [];
27672
27766
  for (const m of mergedResult.methods) {
@@ -28187,6 +28281,7 @@ var init_specs2 = __esm({
28187
28281
  "note",
28188
28282
  "caller"
28189
28283
  ]);
28284
+ DELTA_JUMP_PINS = /* @__PURE__ */ Symbol("jumps written by this delta");
28190
28285
  workspaces = /* @__PURE__ */ new Map();
28191
28286
  lastServedBinding = null;
28192
28287
  }
@@ -30443,17 +30538,17 @@ function memberHop(agentId) {
30443
30538
  const alias = agentId.slice(0, at);
30444
30539
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
30445
30540
  if (!member) return null;
30446
- const relative25 = path28.relative(getProjectRoot(), member.directory).split(path28.sep).join("/");
30447
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative25 };
30541
+ const relative26 = path28.relative(getProjectRoot(), member.directory).split(path28.sep).join("/");
30542
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative26 };
30448
30543
  }
30449
- function throughMount(brief, alias, relative25) {
30450
- const under = (p) => path28.posix.join(relative25, p);
30544
+ function throughMount(brief, alias, relative26) {
30545
+ const under = (p) => path28.posix.join(relative26, p);
30451
30546
  return {
30452
30547
  ...brief,
30453
30548
  agentId: `${alias}::${brief.agentId}`,
30454
30549
  ownedPaths: brief.ownedPaths.map(under),
30455
30550
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
30456
- root: brief.root ? under(brief.root) : relative25
30551
+ root: brief.root ? under(brief.root) : relative26
30457
30552
  };
30458
30553
  }
30459
30554
  function loadRegistry() {
@@ -33843,8 +33938,8 @@ var require_dist = __commonJS({
33843
33938
  walkPackDir(root, path90.join(current2, entry.name), out);
33844
33939
  } else if (entry.isFile()) {
33845
33940
  const absolute = path90.join(current2, entry.name);
33846
- const relative25 = path90.relative(root, absolute).split(path90.sep).join("/");
33847
- out.push({ path: relative25, contents: fs66.readFileSync(absolute) });
33941
+ const relative26 = path90.relative(root, absolute).split(path90.sep).join("/");
33942
+ out.push({ path: relative26, contents: fs66.readFileSync(absolute) });
33848
33943
  }
33849
33944
  }
33850
33945
  }
@@ -34151,19 +34246,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
34151
34246
  const backupDir = path33.join(root, BACKUP_DIR, timestampSlug());
34152
34247
  let backedUp = false;
34153
34248
  for (const rootRel of roots) {
34154
- const relative25 = rootRel === "." ? "" : rootRel;
34155
- const stagedSpecDir = path33.join(stagingDir, relative25, ".wai");
34249
+ const relative26 = rootRel === "." ? "" : rootRel;
34250
+ const stagedSpecDir = path33.join(stagingDir, relative26, ".wai");
34156
34251
  if (!fs19.existsSync(stagedSpecDir)) continue;
34157
- const liveSpecDir = liveSpecDirFor(path33.join(root, relative25));
34252
+ const liveSpecDir = liveSpecDirFor(path33.join(root, relative26));
34158
34253
  if (backupExisting && liveSpecDir && fs19.existsSync(liveSpecDir)) {
34159
- const backupTarget = path33.join(backupDir, relative25, path33.basename(liveSpecDir));
34254
+ const backupTarget = path33.join(backupDir, relative26, path33.basename(liveSpecDir));
34160
34255
  fs19.mkdirSync(path33.dirname(backupTarget), { recursive: true });
34161
34256
  fs19.renameSync(liveSpecDir, backupTarget);
34162
34257
  backedUp = true;
34163
34258
  } else if (liveSpecDir && fs19.existsSync(liveSpecDir)) {
34164
34259
  fs19.rmSync(liveSpecDir, { recursive: true, force: true });
34165
34260
  }
34166
- const target = path33.join(root, relative25, ".wai");
34261
+ const target = path33.join(root, relative26, ".wai");
34167
34262
  fs19.mkdirSync(path33.dirname(target), { recursive: true });
34168
34263
  if (fs19.existsSync(target)) fs19.rmSync(target, { recursive: true, force: true });
34169
34264
  fs19.renameSync(stagedSpecDir, target);
@@ -34806,7 +34901,7 @@ function contextDir(...segments) {
34806
34901
  return aiDir("context", ...segments);
34807
34902
  }
34808
34903
  function hasContext() {
34809
- return pathExists(CONTEXT_PATHS.projectMd());
34904
+ return pathExists(CONTEXT_PATHS.dir());
34810
34905
  }
34811
34906
  function readProjectContext() {
34812
34907
  return readFileOrNull(CONTEXT_PATHS.projectMd());
@@ -40373,7 +40468,7 @@ ${renderChangeReport(report3)}`,
40373
40468
  contract: import_zod11.z.string().describe("The L3 Interface contract ID this implements"),
40374
40469
  sourcePath: import_zod11.z.string().optional().describe("Optional: target source code file path relative to project root \u2014 for a chained subproject's implementation (qualified id), relative to that subproject's root; a path given relative to this root that lands inside the subproject is re-expressed for you"),
40375
40470
  simPath: import_zod11.z.string().optional().describe("Optional: the committed integration-sim harness file (project-relative; N:1 sharing allowed). The validator proves it exists and its import graph wires the REAL modules (this component + each direct dependency; technology adapters may stay faked) \u2014 running it is CI's job. Declaring the first simPath in a subsystem activates MISSING_INTEGRATION_SIM for its other complete non-leaf implementations"),
40376
- technologies: import_zod11.z.array(import_zod11.z.string()).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"]) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components.`),
40471
+ technologies: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ name: import_zod11.z.string(), matches: import_zod11.z.array(import_zod11.z.string()).min(1).describe("The tokens the leakage and contract checks match INSTEAD of the name \u2014 for a technology whose name is also an ordinary word of the tree (the yaml package, the YAML format)") }).strict()])).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"], or [{ "name": "yaml", "matches": ["yaml package", "parseDocument"] }] when the bare name would match a common word) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components.`),
40377
40472
  injectedParams: import_zod11.z.array(import_zod11.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). Only a LEADING run is dropped \u2014 one of these names appearing after the contract's own parameters is an argument in the middle of the caller's list, not wiring"),
40378
40473
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
40379
40474
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
@@ -40652,7 +40747,7 @@ ${testsBlock}` : ""}`, deletion);
40652
40747
  inputSchema: {
40653
40748
  kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project export table, each entry a re-export {from, component, interface, typeDef, as, name, type, details, audience: project|department|instance|partner|external} \u2014 from alone re-exports everything a subsystem exports, and a legacy {id, subsystem} reads as {as, from})"),
40654
40749
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
40655
- delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component, and by from+item+interface+as for a re-export or a type export), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
40750
+ delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component, and by from+item+interface+as for a re-export or a type export), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. Only STORED jumps relocate: a jump the delta itself writes (on an inserted, appended or edited step) names its target in the numbering the WHOLE delta leaves behind, after all of its inserts and deletes, and no later step of the same delta moves it. A method the delta ADDS without a narrative is an intent-level method with none ([]). A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
40656
40751
  dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
40657
40752
  },
40658
40753
  outputSchema: specChangeReportOutput
@@ -41202,8 +41297,10 @@ var mcp_exports = {};
41202
41297
  __export(mcp_exports, {
41203
41298
  claudeMcpConfigPath: () => claudeMcpConfigPath,
41204
41299
  detectHostedMcpSource: () => detectHostedMcpSource,
41300
+ findLegacyPlugin: () => findLegacyPlugin,
41205
41301
  normalizeBackend: () => normalizeBackend,
41206
- removeLegacyGlobalPlugin: () => removeLegacyGlobalPlugin,
41302
+ planMcpInstall: () => planMcpInstall,
41303
+ retireLegacyPlugin: () => retireLegacyPlugin,
41207
41304
  runMcpInstall: () => runMcpInstall,
41208
41305
  runMcpServe: () => runMcpServe,
41209
41306
  runMcpStatus: () => runMcpStatus,
@@ -41335,33 +41432,27 @@ function normalizeBackend(input) {
41335
41432
  `Unknown --backend "${input}". Supported: claude (Claude Code) or gemini (a.k.a. agy / antigravity \u2014 both are Gemini-based). MCP auto-registration for codex / cursor / copilot is not yet supported; configure those manually.`
41336
41433
  );
41337
41434
  }
41338
- async function runMcpInstall(options = {}) {
41435
+ function resolveBackends(options) {
41436
+ if (options.backend) return [normalizeBackend(options.backend)];
41437
+ const backends = [];
41438
+ try {
41439
+ const config = loadProjectConfig();
41440
+ const enabledTypes = (config?.targets ?? []).filter((t) => !("enabled" in t) || t.enabled).map((t) => t.type);
41441
+ if (enabledTypes.includes("claude")) backends.push("claude");
41442
+ if (enabledTypes.includes("gemini") || enabledTypes.includes("agy")) backends.push("gemini");
41443
+ } catch {
41444
+ }
41445
+ return backends.length ? backends : ["claude"];
41446
+ }
41447
+ function resolveInstallTargets(options) {
41339
41448
  assertProjectInitialized();
41340
41449
  if (options.configDir && !options.backend) {
41341
41450
  throw new WaironError("--config-dir requires --backend (claude or gemini), since a config dir is agent-specific.");
41342
41451
  }
41343
- let backends = [];
41344
- if (options.backend) {
41345
- backends = [normalizeBackend(options.backend)];
41346
- } else {
41347
- try {
41348
- const config = loadProjectConfig();
41349
- const enabledTypes = (config?.targets ?? []).filter((t) => !("enabled" in t) || t.enabled).map((t) => t.type);
41350
- if (enabledTypes.includes("claude")) {
41351
- backends.push("claude");
41352
- }
41353
- if (enabledTypes.includes("gemini") || enabledTypes.includes("agy")) {
41354
- backends.push("gemini");
41355
- }
41356
- } catch {
41357
- }
41358
- if (backends.length === 0) {
41359
- backends = ["claude"];
41360
- }
41361
- }
41362
- for (const backend of backends) {
41452
+ const root = getProjectRoot();
41453
+ return resolveBackends(options).map((backend) => {
41363
41454
  if (options.configDir) validateConfigDir(options.configDir, backend);
41364
- const useGlobal = options.global || !!options.configDir;
41455
+ const useGlobal = !!(options.global || options.configDir);
41365
41456
  let configBase;
41366
41457
  let settingsPath;
41367
41458
  if (backend === "gemini") {
@@ -41385,40 +41476,75 @@ async function runMcpInstall(options = {}) {
41385
41476
  }
41386
41477
  }
41387
41478
  const mcpServers = settings["mcpServers"] ?? {};
41388
- const agentLabel = backend === "gemini" ? "Antigravity" : "Claude";
41389
41479
  const isPackaged = typeof process.pkg !== "undefined";
41390
41480
  const scriptPath = process.argv[1] ? path48.resolve(process.argv[1]).replace(/\\/g, "/") : null;
41391
41481
  const useDirectNode = !isPackaged && scriptPath && (scriptPath.endsWith(".js") || scriptPath.endsWith(".ts"));
41392
41482
  const env = useGlobal ? {} : { WAIRON_PROJECT_DIR: process.cwd().replace(/\\/g, "/") };
41393
41483
  const desiredEntry = options.hostedUrl ? hostedMcpEntry(options) : useDirectNode ? { command: "node", args: [scriptPath, "mcp", "serve"], env } : { command: "wairon", args: ["mcp", "serve"], env };
41394
41484
  const existingEntry = mcpServers["wairon"];
41395
- if (existingEntry && JSON.stringify(existingEntry) === JSON.stringify(desiredEntry)) {
41396
- logger.info(`wairon MCP server already registered (up to date) for ${agentLabel} in ${import_chalk3.default.gray(settingsPath)}.`);
41485
+ return {
41486
+ backend,
41487
+ path: settingsPath,
41488
+ outsideProject: isOutsideRoot(root, settingsPath),
41489
+ ...existingEntry !== void 0 ? { before: existingEntry } : {},
41490
+ after: desiredEntry,
41491
+ upToDate: !!existingEntry && JSON.stringify(existingEntry) === JSON.stringify(desiredEntry),
41492
+ configBase,
41493
+ settings,
41494
+ useGlobal
41495
+ };
41496
+ });
41497
+ }
41498
+ function planMcpInstall(options) {
41499
+ return resolveInstallTargets(options).map(({ backend, path: file, outsideProject, before, after, upToDate }) => ({
41500
+ backend,
41501
+ path: file,
41502
+ outsideProject,
41503
+ ...before !== void 0 ? { before } : {},
41504
+ after,
41505
+ upToDate
41506
+ }));
41507
+ }
41508
+ async function runMcpInstall(options = {}) {
41509
+ for (const target of resolveInstallTargets(options)) {
41510
+ const agentLabel = target.backend === "gemini" ? "Antigravity" : "Claude";
41511
+ if (target.upToDate) {
41512
+ logger.info(`wairon MCP server already registered (up to date) for ${agentLabel} in ${import_chalk3.default.gray(target.path)}.`);
41397
41513
  continue;
41398
41514
  }
41399
- const wasStale = !!existingEntry;
41400
- mcpServers["wairon"] = desiredEntry;
41401
- settings["mcpServers"] = mcpServers;
41402
- if (!fs28.existsSync(configBase)) fs28.mkdirSync(configBase, { recursive: true });
41403
- fs28.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
41404
- logger.success(`wairon MCP server ${wasStale ? "updated (was stale)" : "registered"} for ${agentLabel} in ${import_chalk3.default.cyan(settingsPath)}.`);
41405
- logger.blank();
41406
- logger.info("AI tools using this config will have access to these wairon tools:");
41407
- logger.info(" listAgents \xB7 getAgent \xB7 listDomains \xB7 validateTopology \xB7 getProjectConfig");
41408
- logger.info(" sdd_initialize_system \xB7 sdd_add_subsystem \xB7 sdd_add_component \xB7 sdd_define_interface");
41409
- logger.info(" sdd_write_narrative \xB7 sdd_validate_tree \xB7 sdd_get_status");
41410
- logger.blank();
41411
- const restartApp = backend === "gemini" ? "Antigravity CLI (agy)" : "claude";
41412
- logger.info(`Restart ${import_chalk3.default.bold(restartApp)} (or reload MCP servers) to activate.`);
41413
- if (backend === "claude" && !useGlobal) {
41414
- logger.warn('Claude shows project .mcp.json servers as "\u23F8 Pending approval" \u2014 approve wairon once on next launch (or run `claude` and accept the trust prompt).');
41415
- }
41416
- if (backend === "gemini" && !useGlobal) {
41417
- logger.warn("Note: Antigravity (agy) reads MCP servers from its global ~/.gemini/antigravity-cli/mcp_config.json, not this project file.");
41418
- logger.warn("If the agent cannot see the sdd_* tools, run: " + import_chalk3.default.bold("wairon mcp install --backend gemini --global"));
41419
- }
41420
- logger.blank();
41515
+ writeInstallTarget(target);
41516
+ reportInstalled(target, agentLabel);
41517
+ }
41518
+ }
41519
+ function writeInstallTarget(target) {
41520
+ const mcpServers = target.settings["mcpServers"] ?? {};
41521
+ mcpServers["wairon"] = target.after;
41522
+ target.settings["mcpServers"] = mcpServers;
41523
+ if (!fs28.existsSync(target.configBase)) fs28.mkdirSync(target.configBase, { recursive: true });
41524
+ if (target.outsideProject && fs28.existsSync(target.path)) {
41525
+ logger.info(`Backed up ${import_chalk3.default.gray(target.path)} to ${import_chalk3.default.cyan(backupBeside(target.path))} before replacing it (outside the project root).`);
41421
41526
  }
41527
+ fs28.writeFileSync(target.path, JSON.stringify(target.settings, null, 2) + "\n");
41528
+ }
41529
+ function reportInstalled(target, agentLabel) {
41530
+ const wasStale = target.before !== void 0;
41531
+ logger.success(`wairon MCP server ${wasStale ? "updated (was stale)" : "registered"} for ${agentLabel} in ${import_chalk3.default.cyan(target.path)}.`);
41532
+ logger.blank();
41533
+ logger.info("AI tools using this config will have access to these wairon tools:");
41534
+ logger.info(" listAgents \xB7 getAgent \xB7 listDomains \xB7 validateTopology \xB7 getProjectConfig");
41535
+ logger.info(" sdd_initialize_system \xB7 sdd_add_subsystem \xB7 sdd_add_component \xB7 sdd_define_interface");
41536
+ logger.info(" sdd_write_narrative \xB7 sdd_validate_tree \xB7 sdd_get_status");
41537
+ logger.blank();
41538
+ const restartApp = target.backend === "gemini" ? "Antigravity CLI (agy)" : "claude";
41539
+ logger.info(`Restart ${import_chalk3.default.bold(restartApp)} (or reload MCP servers) to activate.`);
41540
+ if (target.backend === "claude" && !target.useGlobal) {
41541
+ logger.warn('Claude shows project .mcp.json servers as "\u23F8 Pending approval" \u2014 approve wairon once on next launch (or run `claude` and accept the trust prompt).');
41542
+ }
41543
+ if (target.backend === "gemini" && !target.useGlobal) {
41544
+ logger.warn("Note: Antigravity (agy) reads MCP servers from its global ~/.gemini/antigravity-cli/mcp_config.json, not this project file.");
41545
+ logger.warn("If the agent cannot see the sdd_* tools, run: " + import_chalk3.default.bold("wairon mcp install --backend gemini --global"));
41546
+ }
41547
+ logger.blank();
41422
41548
  }
41423
41549
  async function runMcpStatus() {
41424
41550
  assertProjectInitialized();
@@ -41475,19 +41601,25 @@ function hostedMcpEntry(options) {
41475
41601
  if (options.hostedProject) headers["X-Wairon-Project"] = options.hostedProject;
41476
41602
  return { type: "http", url: `${base}/mcp`, headers };
41477
41603
  }
41478
- function removeLegacyGlobalPlugin() {
41479
- const home = process.env["USERPROFILE"] ?? process.env["HOME"] ?? os8.homedir();
41480
- const pluginDir = path48.join(home, ".gemini", "config", "plugins", "wairon");
41604
+ function legacyPluginDir() {
41605
+ return path48.join(geminiGlobalDir(), "config", "plugins", "wairon");
41606
+ }
41607
+ function findLegacyPlugin() {
41608
+ const dir = legacyPluginDir();
41609
+ return fs28.existsSync(dir) ? dir : null;
41610
+ }
41611
+ function retireLegacyPlugin() {
41612
+ const dir = findLegacyPlugin();
41613
+ if (!dir) return null;
41614
+ const backup = path48.join(path48.dirname(path48.dirname(dir)), `wairon-plugin.wairon-backup-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`);
41481
41615
  try {
41482
- if (fs28.existsSync(pluginDir)) {
41483
- fs28.rmSync(pluginDir, { recursive: true, force: true });
41484
- logger.info(`Removed legacy global Antigravity plugin at ${import_chalk3.default.gray(pluginDir)} (it collides with the wairon MCP server).`);
41485
- return true;
41486
- }
41616
+ fs28.renameSync(dir, backup);
41617
+ logger.info(`Moved the legacy global Antigravity plugin ${import_chalk3.default.gray(dir)} aside to ${import_chalk3.default.cyan(backup)} (it collides with the wairon MCP server).`);
41618
+ return backup;
41487
41619
  } catch (e) {
41488
- logger.warn(`Could not remove legacy wairon Antigravity plugin: ${String(e)}`);
41620
+ logger.warn(`Could not move the legacy wairon Antigravity plugin aside: ${String(e)}`);
41621
+ return null;
41489
41622
  }
41490
- return false;
41491
41623
  }
41492
41624
  var fs28, os8, path48, import_chalk3;
41493
41625
  var init_mcp = __esm({
@@ -42399,10 +42531,12 @@ init_base();
42399
42531
  init_fs();
42400
42532
  init_project();
42401
42533
  var WAIRON_AGENT_FILE = /-(owner|implementer|architect)\.md$/;
42402
- function pruneStaleAgents(expectedPaths, scanDirs) {
42534
+ function pruneStaleAgents(expectedPaths, scanDirs, reach2) {
42403
42535
  const dirs = new Set(scanDirs ?? [...expectedPaths].map((p) => path50.dirname(p)));
42404
42536
  let pruned = 0;
42405
42537
  for (const dir of dirs) {
42538
+ const outside = reach2 !== void 0 && isOutsideRoot(reach2.root, dir);
42539
+ if (outside && !reach2.global) continue;
42406
42540
  let entries;
42407
42541
  try {
42408
42542
  entries = fs29.readdirSync(dir);
@@ -42422,6 +42556,7 @@ function pruneStaleAgents(expectedPaths, scanDirs) {
42422
42556
  }
42423
42557
  }
42424
42558
  if (!owned) continue;
42559
+ if (outside) logger.info(`Backed up ${full} to ${backupBeside(full)} before pruning it (outside the project root).`);
42425
42560
  fs29.unlinkSync(full);
42426
42561
  pruned++;
42427
42562
  logger.verbose(`Pruned stale: ${full}`);
@@ -42429,6 +42564,28 @@ function pruneStaleAgents(expectedPaths, scanDirs) {
42429
42564
  }
42430
42565
  return pruned;
42431
42566
  }
42567
+ function targetsOutsideRoot(projectConfig, root) {
42568
+ const out = [];
42569
+ for (const target of projectConfig.targets) {
42570
+ if (typeof target === "string" || !("outputDir" in target) || !target.outputDir) continue;
42571
+ if ("enabled" in target && target.enabled === false) continue;
42572
+ const dir = path50.resolve(root, target.outputDir);
42573
+ if (isOutsideRoot(root, dir)) out.push({ type: target.type, dir });
42574
+ }
42575
+ return out;
42576
+ }
42577
+ function announceOutsideTargets(projectConfig, reach2) {
42578
+ const outside = targetsOutsideRoot(projectConfig, reach2.root);
42579
+ if (!outside.length) return [];
42580
+ if (reach2.global) {
42581
+ for (const t of outside) logger.info(`Target "${t.type}" writes agent files outside the project root, to ${t.dir} (--global): each file replaced or pruned there is backed up beside itself first.`);
42582
+ return [];
42583
+ }
42584
+ for (const t of outside) {
42585
+ logger.warn(`Skipped target "${t.type}": its agent files would land outside the project root, in ${t.dir}. Nothing was written or pruned there \u2014 re-run with --global to write there (each replaced file is backed up beside itself).`);
42586
+ }
42587
+ return outside.map((t) => t.type);
42588
+ }
42432
42589
  async function runGenerate(options = {}) {
42433
42590
  await generateLayer(options);
42434
42591
  const scoped = options.root || options.domain || options.domains;
@@ -42452,13 +42609,15 @@ async function generateLayer(options = {}) {
42452
42609
  logger.warn("No agents resolved from the spec tree. Define subsystems and components first \u2014 see `wairon status`.");
42453
42610
  return;
42454
42611
  }
42612
+ const reach2 = { root: getProjectRoot(), global: options.global === true };
42613
+ const skippedTargets = announceOutsideTargets(projectConfig, reach2);
42455
42614
  if (projectConfig.rules.materializeAgentFiles) {
42456
- materializeAgentLayer(agents, projectConfig, options);
42615
+ materializeAgentLayer(agents, projectConfig, options, reach2, skippedTargets);
42457
42616
  } else if (options.dryRun) {
42458
42617
  logger.info("Dry run \u2014 rules.materializeAgentFiles is off: no agent files are written; leftover managed files would be removed.");
42459
42618
  } else if (options.prune !== false) {
42460
42619
  const candidateDirs = [...resolveExpectedOutputPaths(agents, projectConfig)].map((p) => path50.dirname(p));
42461
- const removed = pruneStaleAgents(/* @__PURE__ */ new Set(), candidateDirs);
42620
+ const removed = pruneStaleAgents(/* @__PURE__ */ new Set(), candidateDirs, reach2);
42462
42621
  if (removed > 0) {
42463
42622
  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.`);
42464
42623
  }
@@ -42483,8 +42642,17 @@ async function generateLayer(options = {}) {
42483
42642
  logger.warn(`Failed to re-inject AI guides: ${String(err)}`);
42484
42643
  }
42485
42644
  }
42486
- function materializeAgentLayer(agents, projectConfig, options) {
42487
- const filterTargets = options.target ? [options.target] : void 0;
42645
+ function backUpOutsideFiles(agents, projectConfig, reach2) {
42646
+ const backups = /* @__PURE__ */ new Map();
42647
+ if (!reach2.global) return backups;
42648
+ for (const file of resolveExpectedOutputPaths(agents, projectConfig)) {
42649
+ if (isOutsideRoot(reach2.root, file) && pathExists(file)) backups.set(path50.resolve(file), backupBeside(file));
42650
+ }
42651
+ return backups;
42652
+ }
42653
+ function materializeAgentLayer(agents, projectConfig, options, reach2, skippedTargets) {
42654
+ const configured = activeTargetTypes(projectConfig);
42655
+ const filterTargets = options.target ? [options.target].filter((t) => !skippedTargets.includes(t)) : skippedTargets.length ? configured.filter((t) => !skippedTargets.includes(t)) : void 0;
42488
42656
  let filterDomainIds;
42489
42657
  if (options.root) {
42490
42658
  filterDomainIds = ["root"];
@@ -42500,6 +42668,7 @@ function materializeAgentLayer(agents, projectConfig, options) {
42500
42668
  }
42501
42669
  logger.info(`Generating ${agentPool.length} agent(s)...`);
42502
42670
  logger.blank();
42671
+ const backups = options.dryRun ? /* @__PURE__ */ new Map() : backUpOutsideFiles(agents, projectConfig, reach2);
42503
42672
  const summaries = generateAll(agents, projectConfig, {
42504
42673
  filterTargets,
42505
42674
  filterDomainIds,
@@ -42518,13 +42687,19 @@ function materializeAgentLayer(agents, projectConfig, options) {
42518
42687
  } else {
42519
42688
  logger.success(`Written: ${result.outputPath}`);
42520
42689
  written++;
42690
+ const backup = backups.get(path50.resolve(result.outputPath));
42691
+ if (backup) {
42692
+ logger.info(`Backed up the replaced ${result.outputPath} to ${backup}.`);
42693
+ backups.delete(path50.resolve(result.outputPath));
42694
+ }
42521
42695
  }
42522
42696
  }
42523
42697
  }
42524
42698
  }
42699
+ for (const backup of backups.values()) fs29.rmSync(backup, { force: true });
42525
42700
  let prunedCount = 0;
42526
42701
  if (!options.dryRun && options.prune !== false) {
42527
- prunedCount = pruneStaleAgents(resolveExpectedOutputPaths(agents, projectConfig));
42702
+ prunedCount = pruneStaleAgents(resolveExpectedOutputPaths(agents, projectConfig), void 0, reach2);
42528
42703
  }
42529
42704
  logger.blank();
42530
42705
  if (options.dryRun) {
@@ -44279,9 +44454,9 @@ async function runDoctor(options = {}) {
44279
44454
  line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
44280
44455
  }
44281
44456
  }
44282
- const pluginDir = path54.join(os10.homedir(), ".gemini", "config", "plugins", "wairon");
44283
- if (fs32.existsSync(pluginDir)) {
44284
- line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
44457
+ const pluginDir = findLegacyPlugin();
44458
+ if (pluginDir) {
44459
+ line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Move it aside with \`wairon doctor --fix --global\` (it is outside the project, so --fix asks first).`);
44285
44460
  }
44286
44461
  logger.blank();
44287
44462
  printSummary(tally);
@@ -44358,28 +44533,70 @@ async function applyFixes(options, tally) {
44358
44533
  console.log(` ${icon("error")} Narrative-step repair failed: ${e instanceof Error ? e.message : String(e)}`);
44359
44534
  }
44360
44535
  await migrateChaining(options, tally);
44361
- const { runMcpInstall: runMcpInstall2 } = (init_mcp(), __toCommonJS(mcp_exports));
44362
- if (targets.includes("claude")) {
44536
+ await repairMcpRegistration(options, targets);
44537
+ logger.blank();
44538
+ }
44539
+ async function repairMcpRegistration(options, targets) {
44540
+ const installs = [];
44541
+ if (targets.includes("claude")) installs.push({ backend: "claude", global: false });
44542
+ const gemini = targets.includes("gemini") || targets.includes("agy");
44543
+ if (gemini) installs.push({ backend: "gemini", global: targets.includes("agy") });
44544
+ const outside = [];
44545
+ for (const install of installs) {
44546
+ let writes;
44363
44547
  try {
44364
- await runMcpInstall2({ backend: "claude", global: false });
44548
+ writes = planMcpInstall(install);
44365
44549
  } catch (e) {
44366
- console.log(` ${icon("warn")} MCP install for claude failed: ${e instanceof Error ? e.message : String(e)}`);
44550
+ console.log(` ${icon("warn")} MCP install for ${install.backend} could not be planned: ${e instanceof Error ? e.message : String(e)}`);
44551
+ continue;
44367
44552
  }
44553
+ if (writes.some((w) => w.outsideProject && !w.upToDate)) outside.push({ install, writes });
44554
+ else await installMcp(install);
44368
44555
  }
44369
- if (targets.includes("gemini") || targets.includes("agy")) {
44370
- const global = targets.includes("agy");
44371
- try {
44372
- await runMcpInstall2({ backend: "gemini", global });
44373
- } catch (e) {
44374
- console.log(` ${icon("warn")} MCP install for gemini failed: ${e instanceof Error ? e.message : String(e)}`);
44375
- }
44376
- try {
44377
- const { removeLegacyGlobalPlugin: removeLegacyGlobalPlugin2 } = (init_mcp(), __toCommonJS(mcp_exports));
44378
- if (removeLegacyGlobalPlugin2()) console.log(` ${icon("ok")} Removed the legacy global Antigravity plugin (name collided with the MCP server).`);
44379
- } catch {
44556
+ const plugin = gemini ? findLegacyPlugin() : null;
44557
+ if (outside.length === 0 && !plugin) return;
44558
+ printOutsideWrites(outside, plugin);
44559
+ if (!await confirmOutsideWrites(options)) return;
44560
+ for (const planned of outside) await installMcp(planned.install);
44561
+ if (plugin) retireLegacyPlugin();
44562
+ }
44563
+ async function installMcp(install) {
44564
+ try {
44565
+ await runMcpInstall(install);
44566
+ } catch (e) {
44567
+ console.log(` ${icon("warn")} MCP install for ${install.backend} failed: ${e instanceof Error ? e.message : String(e)}`);
44568
+ }
44569
+ }
44570
+ function printOutsideWrites(outside, plugin) {
44571
+ console.log(` ${icon("warn")} ${import_chalk12.default.bold("Writes outside this project")} \u2014 machine-wide state every project on this machine shares:`);
44572
+ for (const { writes } of outside) {
44573
+ for (const w of writes.filter((x) => x.outsideProject && !x.upToDate)) {
44574
+ console.log(` ${w.path} (${w.backend} MCP registration)`);
44575
+ console.log(` mcpServers.wairon now: ${describeMcpEntry(w.before)}`);
44576
+ console.log(` mcpServers.wairon new: ${describeMcpEntry(w.after)}`);
44577
+ console.log(" the file is backed up beside itself before it is replaced");
44380
44578
  }
44381
44579
  }
44382
- logger.blank();
44580
+ if (plugin) console.log(` ${plugin} (legacy Antigravity plugin) \u2014 moved aside, not deleted`);
44581
+ }
44582
+ function describeMcpEntry(entry) {
44583
+ if (entry === void 0 || entry === null) return "(none)";
44584
+ const text3 = JSON.stringify(entry, (key, value) => key === "Authorization" && typeof value === "string" ? value.replace(/^(\S+\s+).+$/, "$1***") : value);
44585
+ const LIMIT = 200;
44586
+ return text3.length <= LIMIT ? text3 : `${text3.slice(0, LIMIT)}\u2026 (${text3.length} characters)`;
44587
+ }
44588
+ async function confirmOutsideWrites(options) {
44589
+ if (options.yes && options.global) return true;
44590
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
44591
+ const why = options.global ? "no terminal to confirm them" : options.yes ? "--yes does not cover a write outside the project without --global" : "no terminal to confirm them and no --global";
44592
+ console.log(` ${icon("warn")} Skipped the writes outside this project: ${why}. Nothing outside the project was written \u2014 re-run with \`wairon doctor --fix --yes --global\` to apply them.`);
44593
+ return false;
44594
+ }
44595
+ const { confirmed } = await import_inquirer5.default.prompt([
44596
+ { type: "confirm", name: "confirmed", message: "Apply the writes outside this project listed above?", default: false }
44597
+ ]);
44598
+ if (!confirmed) console.log(` ${icon("warn")} Skipped the writes outside this project. Nothing outside the project was written.`);
44599
+ return confirmed;
44383
44600
  }
44384
44601
  function reportOnly(options) {
44385
44602
  if (options.fix) {
@@ -44571,7 +44788,8 @@ function printChainingPlan(migration) {
44571
44788
  }
44572
44789
  var projectLabel2 = (project2) => project2 === "" ? "(top root)" : project2;
44573
44790
  function printProjectMigration(p) {
44574
- console.log(` ${import_chalk12.default.bold(projectLabel2(p.project))} ${import_chalk12.default.gray(p.directory)}`);
44791
+ const above = isOutsideRoot(getProjectRoot(), p.directory) ? import_chalk12.default.yellow(" (outside this project's root \u2014 the family above it)") : "";
44792
+ console.log(` ${import_chalk12.default.bold(projectLabel2(p.project))} ${import_chalk12.default.gray(p.directory)}${above}`);
44575
44793
  if (p.idToWrite) console.log(` id: declare "${p.idToWrite}"`);
44576
44794
  if (p.createsSystem) console.log(" L0: create a minimal one \u2014 the project exports and has none");
44577
44795
  for (const e of p.exports) console.log(` L0 entry: ${describeEntry(e)}`);
@@ -44665,13 +44883,13 @@ function writeArtifact(dest, content) {
44665
44883
  fs33.writeFileSync(dest, content, "utf-8");
44666
44884
  }
44667
44885
  function parseSequenceRef(ref) {
44668
- const sep16 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
44669
- if (sep16 <= 0 || sep16 === ref.length - 1) {
44886
+ const sep17 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
44887
+ if (sep17 <= 0 || sep17 === ref.length - 1) {
44670
44888
  throw new WaironError(
44671
44889
  `Invalid --sequence reference "${ref}". Use <componentId>:<methodName> (e.g. billing-portal:authorize).`
44672
44890
  );
44673
44891
  }
44674
- return { component: ref.slice(0, sep16), method: ref.slice(sep16 + 1) };
44892
+ return { component: ref.slice(0, sep17), method: ref.slice(sep17 + 1) };
44675
44893
  }
44676
44894
  async function runDiagram(rawOptions = {}) {
44677
44895
  assertProjectInitialized();
@@ -45074,7 +45292,7 @@ async function installPack(source, yes) {
45074
45292
  const update = await confirmFloatingUpdate(extracted.dir, yes);
45075
45293
  if (update === "declined") return;
45076
45294
  const installed = installPackFromDirectory(extracted.dir, source);
45077
- reportInstalled(installed, source, update, yes);
45295
+ reportInstalled2(installed, source, update, yes);
45078
45296
  } catch (e) {
45079
45297
  logger.error(e instanceof Error ? e.message : String(e));
45080
45298
  process.exitCode = 1;
@@ -45112,7 +45330,7 @@ async function installPack(source, yes) {
45112
45330
  try {
45113
45331
  const update = await confirmFloatingUpdate(sourceDir, yes);
45114
45332
  if (update === "declined") return;
45115
- reportInstalled(installPackFromDirectory(sourceDir, origin), origin, update, yes);
45333
+ reportInstalled2(installPackFromDirectory(sourceDir, origin), origin, update, yes);
45116
45334
  } catch (e) {
45117
45335
  logger.error(e instanceof Error ? e.message : String(e));
45118
45336
  process.exitCode = 1;
@@ -45141,7 +45359,7 @@ async function confirmFloatingUpdate(sourceDir, yes) {
45141
45359
  logger.info("Nothing was installed.");
45142
45360
  return "declined";
45143
45361
  }
45144
- function reportInstalled(installed, origin, movedFloating = false, yes) {
45362
+ function reportInstalled2(installed, origin, movedFloating = false, yes) {
45145
45363
  logger.success(`Installed ${installed.name} v${installed.version} into the pack store.`);
45146
45364
  console.log(` ${import_chalk16.default.dim(installed.path)}`);
45147
45365
  console.log(` ${import_chalk16.default.dim(installed.digest)}`);
@@ -61483,8 +61701,9 @@ program.command("init").description("Initialize wairon and bootstrap the SDD Spe
61483
61701
  await addPack(source, false, opts.yes);
61484
61702
  }
61485
61703
  });
61486
- program.command("generate").description("Generate agent output files from the spec tree").option("--target <type>", "limit to a specific target (claude|gemini|custom)").option("--domain <id>", "limit to agents in a single domain").option("--domains <ids>", "limit to a comma-separated list of domain ids").option("--root", "only generate root-level agents").option("--family", "also generate each member's own layer in its own root (generate never cascades otherwise)").option("--no-recurse", "accepted for one release: generating only this project's layer is now the default").option("--no-prune", "do not remove wairon-managed agent files that are no longer in the topology").option("--dry-run", "preview what would be generated without writing files").action(async (opts) => {
61704
+ program.command("generate").description("Generate agent output files from the spec tree").option("--target <type>", "limit to a specific target (claude|gemini|custom)").option("--domain <id>", "limit to agents in a single domain").option("--domains <ids>", "limit to a comma-separated list of domain ids").option("--root", "only generate root-level agents").option("--family", "also generate each member's own layer in its own root (generate never cascades otherwise)").option("--no-recurse", "accepted for one release: generating only this project's layer is now the default").option("--no-prune", "do not remove wairon-managed agent files that are no longer in the topology").option("--dry-run", "preview what would be generated without writing files").option("--global", "also write (and prune) a target whose output directory resolves outside the project root, backing up each file replaced there").action(async (opts) => {
61487
61705
  await runGenerate({
61706
+ global: opts.global === true,
61488
61707
  target: opts.target,
61489
61708
  domain: opts.domain,
61490
61709
  domains: opts.domains,
@@ -61611,8 +61830,8 @@ program.command("validate").description("Validate the project configuration and
61611
61830
  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) => {
61612
61831
  await statusCommand(opts);
61613
61832
  });
61614
- 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)").action(async (opts) => {
61615
- await runDoctor({ fix: opts.fix, report: opts.report, yes: opts.yes });
61833
+ 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) => {
61834
+ await runDoctor({ fix: opts.fix, report: opts.report, yes: opts.yes, global: opts.global === true });
61616
61835
  });
61617
61836
  async function runRules() {
61618
61837
  await listRules3();