@wairon/cli 5.0.2-dev.13 → 5.0.2-dev.15

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.0.2-dev.13";
68
+ WAIRON_VERSION = "5.0.2-dev.15";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -458,7 +458,7 @@ var init_domain = __esm({
458
458
  });
459
459
 
460
460
  // src/models/project.ts
461
- var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProjectConfigSchema;
461
+ var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
462
462
  var init_project = __esm({
463
463
  "src/models/project.ts"() {
464
464
  "use strict";
@@ -582,6 +582,24 @@ var init_project = __esm({
582
582
  /** Base directory containing SDD specification files, relative to project root */
583
583
  specsDir: import_zod3.z.string().default(".wai/specs")
584
584
  });
585
+ ProfileSelectionSubjectSchema = import_zod3.z.object({
586
+ userId: import_zod3.z.string(),
587
+ kind: import_zod3.z.string(),
588
+ issuer: import_zod3.z.string(),
589
+ externalSubject: import_zod3.z.string().optional(),
590
+ displayName: import_zod3.z.string().optional(),
591
+ email: import_zod3.z.string().optional()
592
+ });
593
+ ProjectProfileSelectionSchema = import_zod3.z.object({
594
+ /** Selected architectural profile ids. The first resolvable one is applied as projectType. */
595
+ profileIds: import_zod3.z.array(import_zod3.z.string()).default([]),
596
+ /** Pack names the governing policy requires for this project. */
597
+ requiredPackNames: import_zod3.z.array(import_zod3.z.string()).default([]),
598
+ /** Pack names applied by default unless explicitly overridden. */
599
+ defaultPackNames: import_zod3.z.array(import_zod3.z.string()).optional(),
600
+ selectedBy: ProfileSelectionSubjectSchema.optional(),
601
+ selectedAt: import_zod3.z.string()
602
+ });
585
603
  ProjectConfigSchema = import_zod3.z.object({
586
604
  /**
587
605
  * Schema version — used to detect incompatible config formats in future
@@ -622,6 +640,13 @@ var init_project = __esm({
622
640
  useGlobalPacks: import_zod3.z.boolean().default(true)
623
641
  }).optional(),
624
642
  paths: PathsConfigSchema.default({}),
643
+ /**
644
+ * The profile/pack selection a hosted policy workflow applied to this project.
645
+ * The RECORD of what was chosen; `projectType` above is what actually governs
646
+ * validation. Modeled so the parse/write round trip preserves it (see
647
+ * ProjectProfileSelectionSchema).
648
+ */
649
+ profileSelection: ProjectProfileSelectionSchema.optional(),
625
650
  /**
626
651
  * Path to a directory containing org/user-level default templates.
627
652
  * Resolved before built-in templates but after project-local templates.
@@ -1914,6 +1939,47 @@ var init_loader = __esm({
1914
1939
  }
1915
1940
  });
1916
1941
 
1942
+ // src/core/statehash.ts
1943
+ function computeStateId() {
1944
+ const tree = {
1945
+ system: loadSystemSpec(),
1946
+ subsystems: loadSubsystemSpecs(),
1947
+ components: loadComponentSpecs(),
1948
+ interfaces: loadInterfaceSpecs(),
1949
+ implementations: loadImplementationSpecs(),
1950
+ types: loadTypeSpecs()
1951
+ };
1952
+ const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
1953
+ return { algorithm: "sha256", digest };
1954
+ }
1955
+ function stateIdEquals(a, b) {
1956
+ return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
1957
+ }
1958
+ function canonicalize(value) {
1959
+ return JSON.stringify(sortKeys(value));
1960
+ }
1961
+ function sortKeys(v) {
1962
+ if (Array.isArray(v)) return v.map(sortKeys);
1963
+ if (v && typeof v === "object") {
1964
+ const src = v;
1965
+ const out = {};
1966
+ for (const k of Object.keys(src).sort()) {
1967
+ if (k === "createdAt" || k === "updatedAt") continue;
1968
+ out[k] = sortKeys(src[k]);
1969
+ }
1970
+ return out;
1971
+ }
1972
+ return v;
1973
+ }
1974
+ var crypto;
1975
+ var init_statehash = __esm({
1976
+ "src/core/statehash.ts"() {
1977
+ "use strict";
1978
+ crypto = __toESM(require("crypto"));
1979
+ init_specs2();
1980
+ }
1981
+ });
1982
+
1917
1983
  // src/core/narrative-labels.ts
1918
1984
  function resolveNarrativeLabels(methodName, steps) {
1919
1985
  const errors = [];
@@ -7272,7 +7338,7 @@ var init_extensions = __esm({
7272
7338
  });
7273
7339
 
7274
7340
  // src/core/rules/types.ts
7275
- var BUILTIN_PROFILES;
7341
+ var BUILTIN_PROFILES, PROJECT_KINDS;
7276
7342
  var init_types = __esm({
7277
7343
  "src/core/rules/types.ts"() {
7278
7344
  "use strict";
@@ -7285,6 +7351,7 @@ var init_types = __esm({
7285
7351
  "realtime-embedded",
7286
7352
  "plc-cyclic"
7287
7353
  ];
7354
+ PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
7288
7355
  }
7289
7356
  });
7290
7357
 
@@ -7475,47 +7542,6 @@ var init_filenames = __esm({
7475
7542
  }
7476
7543
  });
7477
7544
 
7478
- // src/core/statehash.ts
7479
- function computeStateId() {
7480
- const tree = {
7481
- system: loadSystemSpec(),
7482
- subsystems: loadSubsystemSpecs(),
7483
- components: loadComponentSpecs(),
7484
- interfaces: loadInterfaceSpecs(),
7485
- implementations: loadImplementationSpecs(),
7486
- types: loadTypeSpecs()
7487
- };
7488
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
7489
- return { algorithm: "sha256", digest };
7490
- }
7491
- function stateIdEquals(a, b) {
7492
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
7493
- }
7494
- function canonicalize(value) {
7495
- return JSON.stringify(sortKeys(value));
7496
- }
7497
- function sortKeys(v) {
7498
- if (Array.isArray(v)) return v.map(sortKeys);
7499
- if (v && typeof v === "object") {
7500
- const src = v;
7501
- const out = {};
7502
- for (const k of Object.keys(src).sort()) {
7503
- if (k === "createdAt" || k === "updatedAt") continue;
7504
- out[k] = sortKeys(src[k]);
7505
- }
7506
- return out;
7507
- }
7508
- return v;
7509
- }
7510
- var crypto;
7511
- var init_statehash = __esm({
7512
- "src/core/statehash.ts"() {
7513
- "use strict";
7514
- crypto = __toESM(require("crypto"));
7515
- init_specs2();
7516
- }
7517
- });
7518
-
7519
7545
  // src/core/openapi.ts
7520
7546
  function schemaFor(typeRef, closureIds) {
7521
7547
  const trimmed = typeRef.trim().replace(/^promise\s*<(.+)>$/i, "$1").trim();
@@ -7969,6 +7995,57 @@ function projectOwnSurface(maxAudience) {
7969
7995
  function projectChildSurface() {
7970
7996
  return projectOwnSurface("project");
7971
7997
  }
7998
+ function localName(id) {
7999
+ return id.split("::").pop();
8000
+ }
8001
+ function projectSubsystemSurface(subsystemId) {
8002
+ const system = loadSystemSpec();
8003
+ if (!system) {
8004
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
8005
+ }
8006
+ const subsystems = loadSubsystemSpecs();
8007
+ const target = subsystems.find((s) => s.id === subsystemId);
8008
+ if (!target) {
8009
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
8010
+ }
8011
+ const components = loadComponentSpecs();
8012
+ const interfaces = loadInterfaceSpecs();
8013
+ const types = loadTypeSpecs();
8014
+ const entries = [];
8015
+ for (const pub of target.publicInterfaces ?? []) {
8016
+ if (!pub.component) continue;
8017
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
8018
+ if (!comp) continue;
8019
+ if (comp.componentType !== "Portal") continue;
8020
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
8021
+ const methods = compInterfaces.flatMap((i) => i.methods);
8022
+ entries.push({
8023
+ id: localName(pub.interface ?? comp.id),
8024
+ name: comp.name,
8025
+ // Family ceiling: a sibling surface is consumable by the system family only.
8026
+ audience: "project",
8027
+ type: pub.type ?? "Custom",
8028
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
8029
+ // refs by their final segment.
8030
+ component: localName(comp.id),
8031
+ methods,
8032
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
8033
+ // Project the backing Portal's auth + basePath so the codec can emit
8034
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
8035
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
8036
+ ...comp.basePath ? { basePath: comp.basePath } : {},
8037
+ details: pub.details ?? ""
8038
+ });
8039
+ }
8040
+ return SurfaceSnapshotSchema.parse({
8041
+ projectName: `${system.name}::${subsystemId}`,
8042
+ origin: "generated",
8043
+ stateId: stateIdString(),
8044
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8045
+ interfaces: entries,
8046
+ types: computeTypeClosure(entries, types)
8047
+ });
8048
+ }
7972
8049
  function listSnapshots(rootDir = getProjectRoot()) {
7973
8050
  const dir = surfacesDir(rootDir);
7974
8051
  if (!fs9.existsSync(dir)) return [];
@@ -7985,10 +8062,13 @@ function listSnapshots(rootDir = getProjectRoot()) {
7985
8062
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
7986
8063
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
7987
8064
  }
8065
+ function snapshotFilename(projectName) {
8066
+ return `${safeFilenamePart(projectName)}.yaml`;
8067
+ }
7988
8068
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7989
8069
  const dir = surfacesDir(rootDir);
7990
8070
  fs9.mkdirSync(dir, { recursive: true });
7991
- const p = path10.join(dir, `${snapshot.projectName}.yaml`);
8071
+ const p = path10.join(dir, snapshotFilename(snapshot.projectName));
7992
8072
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
7993
8073
  return p;
7994
8074
  }
@@ -8061,17 +8141,54 @@ function importSurface(sourcePath, origin) {
8061
8141
  return snapshot;
8062
8142
  }
8063
8143
  function generateChildSnapshots(rootDir = getProjectRoot()) {
8064
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
8144
+ const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
8145
+ const children = topLevel.filter((s) => s.projectPath);
8065
8146
  if (!children.length) return [];
8066
- const snapshot = projectChildSurface();
8147
+ const familySnapshot = projectChildSurface();
8148
+ const siblingSnapshots = /* @__PURE__ */ new Map();
8149
+ const siblingSurface = (subsystemId) => {
8150
+ let snap = siblingSnapshots.get(subsystemId);
8151
+ if (!snap) {
8152
+ snap = projectSubsystemSurface(subsystemId);
8153
+ siblingSnapshots.set(subsystemId, snap);
8154
+ }
8155
+ return snap;
8156
+ };
8067
8157
  const written = [];
8068
8158
  for (const child of children) {
8069
8159
  const childDir = path10.resolve(rootDir, child.projectPath);
8070
8160
  if (!fs9.existsSync(childDir)) continue;
8071
- written.push(saveSnapshot(snapshot, childDir));
8161
+ written.push(saveSnapshot(familySnapshot, childDir));
8162
+ for (const sibling of topLevel) {
8163
+ if (sibling.id === child.id) continue;
8164
+ written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
8165
+ }
8072
8166
  }
8073
8167
  return written;
8074
8168
  }
8169
+ function computeParentStateId(parentRoot) {
8170
+ return computeStateIdAt(parentRoot);
8171
+ }
8172
+ function listExternalInterfaces() {
8173
+ const snapshots = listSnapshots();
8174
+ const chainingParent = resolveChainingParent();
8175
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
8176
+ return snapshots.map((snapshot) => {
8177
+ const generated = snapshot.origin === "generated";
8178
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
8179
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
8180
+ return {
8181
+ projectName: snapshot.projectName,
8182
+ origin: snapshot.origin,
8183
+ sourceKind,
8184
+ generatedAt: snapshot.generatedAt,
8185
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
8186
+ ...snapshot.version ? { version: snapshot.version } : {},
8187
+ freshness,
8188
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
8189
+ };
8190
+ });
8191
+ }
8075
8192
  function surfaceContentKey(snapshot) {
8076
8193
  const { stateId, generatedAt, origin, ...content } = snapshot;
8077
8194
  return JSON.stringify(content);
@@ -8280,7 +8397,8 @@ var init_contracts = __esm({
8280
8397
  "SURFACE_REF_NOT_EXPOSED",
8281
8398
  `Method "${implMethod.name}" in implementation "${impl.id}" dispatches capability "${step.capability}" through cross-tree portal "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not serve that capability on "${resolved.entry.id}".`,
8282
8399
  impl.id,
8283
- isDraftCtx
8400
+ isDraftCtx,
8401
+ true
8284
8402
  );
8285
8403
  }
8286
8404
  continue;
@@ -8337,7 +8455,8 @@ var init_contracts = __esm({
8337
8455
  "SURFACE_REF_NOT_EXPOSED",
8338
8456
  `Method "${implMethod.name}" in implementation "${impl.id}" calls "${step.targetMethod}" on cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not expose that method on "${resolved.entry.id}".`,
8339
8457
  impl.id,
8340
- isDraftCtx
8458
+ isDraftCtx,
8459
+ true
8341
8460
  );
8342
8461
  } else if (step.assertsGuarantees) {
8343
8462
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -8348,7 +8467,8 @@ var init_contracts = __esm({
8348
8467
  "NARRATIVE_SEMANTIC_UNBACKED",
8349
8468
  `Step ${step.stepNumber} of "${implMethod.name}" in implementation "${impl.id}" asserts guarantee "${g}", but the surface snapshot of "${resolved.snapshot.projectName}" does not declare it on "${resolved.entry.id}.${step.targetMethod}".`,
8350
8469
  impl.id,
8351
- isDraftCtx
8470
+ isDraftCtx,
8471
+ true
8352
8472
  );
8353
8473
  }
8354
8474
  }
@@ -9836,7 +9956,8 @@ var init_stereotype_deps = __esm({
9836
9956
  "CROSS_SUBSYSTEM_NON_ADAPTER",
9837
9957
  `Boundary violation: ${comp.componentType} "${comp.id}" depends directly on "${depId}", a surface of project "${resolved.snapshot.projectName}". Only a local client Adapter may cross a project boundary \u2014 route this hop through an Adapter.`,
9838
9958
  comp.id,
9839
- isDraftCtx
9959
+ isDraftCtx,
9960
+ true
9840
9961
  );
9841
9962
  }
9842
9963
  continue;
@@ -10414,14 +10535,14 @@ var init_declarative_assertions = __esm({
10414
10535
  });
10415
10536
 
10416
10537
  // src/core/rules/profiles.ts
10417
- var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS, profilesRule;
10538
+ var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
10418
10539
  var init_profiles = __esm({
10419
10540
  "src/core/rules/profiles.ts"() {
10420
10541
  "use strict";
10421
10542
  init_types();
10422
10543
  BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
10423
10544
  FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
10424
- PROJECT_KINDS = /* @__PURE__ */ new Set(["fullstack", "system-of-systems", "monorepo"]);
10545
+ PROJECT_KINDS2 = new Set(PROJECT_KINDS);
10425
10546
  profilesRule = {
10426
10547
  name: "architectural-profiles",
10427
10548
  description: "Per-profile stereotype constraints: View/FeatureComponent/RouterComponent only in frontend profiles; Actor/Supervisor forbidden in plc-cyclic (single scan cycle); Actor/Supervisor in frontend profiles warned. Extension packs may register custom profiles (family + forbidden/discouraged stereotype lists); unknown profile names are flagged.",
@@ -10445,7 +10566,7 @@ var init_profiles = __esm({
10445
10566
  );
10446
10567
  }
10447
10568
  }
10448
- if (!registered.has(ctx.projectType) && !PROJECT_KINDS.has(ctx.projectType)) {
10569
+ if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
10449
10570
  ctx.addIssue(
10450
10571
  "warning",
10451
10572
  "UNKNOWN_PROFILE",
@@ -11699,11 +11820,11 @@ var init_narrative_antipatterns = __esm({
11699
11820
  const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
11700
11821
  if (memberEdges.length === 0) continue;
11701
11822
  const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
11702
- const path62 = [...keys].sort().join(" \u2192 ");
11823
+ const path61 = [...keys].sort().join(" \u2192 ");
11703
11824
  ctx.addIssue(
11704
11825
  "warning",
11705
11826
  "UNCONDITIONAL_CALL_CYCLE",
11706
- `Call cycle with no guard: ${path62} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
11827
+ `Call cycle with no guard: ${path61} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
11707
11828
  anchor.impl.id,
11708
11829
  memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
11709
11830
  );
@@ -13090,9 +13211,15 @@ function buildRuleContext(opts) {
13090
13211
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
13091
13212
  // Declarative assertions bring their own namespaced codes — lint.allow
13092
13213
  // and severity overrides treat them exactly like builtins.
13093
- ...extensions.assertions.map((a) => a.fullCode)
13214
+ ...extensions.assertions.map((a) => a.fullCode),
13215
+ // Entry-point emitted codes: validateSddTree's chained-subproject pass
13216
+ // raises these AFTER the rule run (it post-processes the aggregated issue
13217
+ // list), so no registered rule declares them — but lint.allow validation
13218
+ // must still recognize them as real codes.
13219
+ "CHAINED_SUBPROJECT_CONTEXT",
13220
+ "UNVERIFIED_EXTERNAL_REF"
13094
13221
  ]);
13095
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
13222
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
13096
13223
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
13097
13224
  return;
13098
13225
  }
@@ -13110,7 +13237,14 @@ function buildRuleContext(opts) {
13110
13237
  if (severity === "warning") return;
13111
13238
  }
13112
13239
  }
13113
- issues.push({ severity, code, message, specId, ...isDraftContext ? { draftContext: true } : {} });
13240
+ issues.push({
13241
+ severity,
13242
+ code,
13243
+ message,
13244
+ specId,
13245
+ ...isDraftContext ? { draftContext: true } : {},
13246
+ ...surfaceResolved ? { surfaceResolved: true } : {}
13247
+ });
13114
13248
  };
13115
13249
  return {
13116
13250
  system,
@@ -13535,24 +13669,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
13535
13669
  for (const rule of ruleSequence()) {
13536
13670
  rule.check(ctx);
13537
13671
  }
13538
- const hasCrossTreeSuspects = issues.some((i) => SUBPROJECT_LENIENT_CODES.has(i.code));
13672
+ const hasCrossTreeSuspects = issues.some(
13673
+ (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
13674
+ );
13539
13675
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
13540
13676
  if (chainingParent) {
13677
+ let unverified = 0;
13541
13678
  let downgraded = 0;
13542
- for (const iss of issues) {
13543
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
13544
- if (iss.severity === "error") {
13545
- iss.severity = "warning";
13546
- downgraded++;
13679
+ for (let at = 0; at < issues.length; at++) {
13680
+ const iss = issues[at];
13681
+ if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
13682
+ issues[at] = {
13683
+ severity: "warning",
13684
+ code: "UNVERIFIED_EXTERNAL_REF",
13685
+ crossTreeContext: true,
13686
+ // --ci waives it (parent root is authoritative)
13687
+ specId: iss.specId,
13688
+ ...iss.agentId ? { agentId: iss.agentId } : {},
13689
+ ...iss.draftContext ? { draftContext: true } : {},
13690
+ message: `Unverified external reference (${iss.code}): ${iss.message} No vendored surface snapshot covers this reference, so it cannot be verified from this chained subproject standalone \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect what this project can consume via \`wairon surface externals\` / sdd_list_external_interfaces.`
13691
+ };
13692
+ unverified++;
13693
+ continue;
13694
+ }
13695
+ if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
13696
+ if (iss.severity === "error") {
13697
+ iss.severity = "warning";
13698
+ downgraded++;
13699
+ }
13700
+ iss.crossTreeContext = true;
13547
13701
  }
13548
- iss.crossTreeContext = true;
13549
13702
  }
13550
- if (downgraded > 0) {
13703
+ if (unverified > 0 || downgraded > 0) {
13704
+ const notes = [];
13705
+ if (unverified > 0) {
13706
+ notes.push(
13707
+ `${unverified} cross-tree reference(s) have no vendored surface snapshot covering them and were reported as UNVERIFIED_EXTERNAL_REF warnings \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect via \`wairon surface externals\` / sdd_list_external_interfaces.`
13708
+ );
13709
+ }
13710
+ if (downgraded > 0) {
13711
+ notes.push(
13712
+ `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
13713
+ );
13714
+ }
13551
13715
  issues.unshift({
13552
13716
  severity: "warning",
13553
13717
  code: "CHAINED_SUBPROJECT_CONTEXT",
13554
13718
  crossTreeContext: true,
13555
- message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${downgraded} reference(s) resolve only in the parent tree (shared types, sibling subsystems, or cross-tree components that live above this root) and were downgraded to warnings \u2014 validating a subproject standalone cannot verify them. Run validation from the parent root for full cross-tree verification.`
13719
+ message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${notes.join(" ")} Full cross-tree verification runs from the parent root.`
13556
13720
  });
13557
13721
  }
13558
13722
  }
@@ -13569,7 +13733,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
13569
13733
  function validateAsComplete(options) {
13570
13734
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
13571
13735
  }
13572
- var SUBPROJECT_LENIENT_CODES;
13736
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
13573
13737
  var init_validation = __esm({
13574
13738
  "src/core/validation.ts"() {
13575
13739
  "use strict";
@@ -13582,8 +13746,7 @@ var init_validation = __esm({
13582
13746
  init_source_analysis();
13583
13747
  init_specs2();
13584
13748
  init_fs();
13585
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
13586
- // reference resolution
13749
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
13587
13750
  "UNDEFINED_TYPE_REFERENCE",
13588
13751
  "INVALID_DEPENDENCY_REFERENCE",
13589
13752
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -13591,8 +13754,9 @@ var init_validation = __esm({
13591
13754
  "UNDECLARED_DEPENDENCY_CALL",
13592
13755
  "INVALID_TRUSTED_LINK",
13593
13756
  "CROSS_SUBSYSTEM_NON_ADAPTER",
13594
- "CROSS_TREE_REF_UNRESOLVED",
13595
- // code↔spec conformance (root-relative sourcePaths / import graph)
13757
+ "CROSS_TREE_REF_UNRESOLVED"
13758
+ ]);
13759
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
13596
13760
  "MISSING_SOURCE_FILE",
13597
13761
  "SOURCE_PATH_ESCAPES_ROOT",
13598
13762
  "MISSING_SOURCE_PATH",
@@ -14134,6 +14298,7 @@ __export(specs_exports, {
14134
14298
  buildProjectGraph: () => buildProjectGraph,
14135
14299
  clearLoaderIssues: () => clearLoaderIssues,
14136
14300
  collectPromotableSpecs: () => collectPromotableSpecs,
14301
+ computeStateIdAt: () => computeStateIdAt,
14137
14302
  deleteComponentSpec: () => deleteComponentSpec,
14138
14303
  deleteGroupSpec: () => deleteGroupSpec,
14139
14304
  deleteImplementationSpec: () => deleteImplementationSpec,
@@ -14166,6 +14331,7 @@ __export(specs_exports, {
14166
14331
  loadTypeSpec: () => loadTypeSpec,
14167
14332
  loadTypeSpecs: () => loadTypeSpecs,
14168
14333
  normalizeComponentLayout: () => normalizeComponentLayout,
14334
+ resolveChainingParent: () => resolveChainingParent,
14169
14335
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
14170
14336
  restoreSpecFiles: () => restoreSpecFiles,
14171
14337
  saveComponentSpec: () => saveComponentSpec,
@@ -14570,6 +14736,19 @@ function dryRunSerializeSpecs(include) {
14570
14736
  function buildProjectGraph(level) {
14571
14737
  return buildGraphModel(level);
14572
14738
  }
14739
+ function resolveChainingParent() {
14740
+ return findChainingParent(getProjectRoot());
14741
+ }
14742
+ function computeStateIdAt(root) {
14743
+ const resolved = path14.resolve(root);
14744
+ return runWithProjectRoot(resolved, () => {
14745
+ workspaceFor(resolved).invalidate();
14746
+ const system = loadSystemSpec();
14747
+ if (!system) return null;
14748
+ const s = computeStateId();
14749
+ return `${s.algorithm}:${s.digest}`;
14750
+ });
14751
+ }
14573
14752
  function deleteTypeSpec(id) {
14574
14753
  return current().deleteTypeSpec(id);
14575
14754
  }
@@ -14613,6 +14792,7 @@ var init_specs2 = __esm({
14613
14792
  path14 = __toESM(require("path"));
14614
14793
  init_loader();
14615
14794
  init_fs();
14795
+ init_statehash();
14616
14796
  init_yaml();
14617
14797
  init_models();
14618
14798
  init_narrative_labels();
@@ -17206,6 +17386,9 @@ function requireSpecs() {
17206
17386
  function requireProvision() {
17207
17387
  return init_provision(), __toCommonJS(provision_exports);
17208
17388
  }
17389
+ function listExternalInterfaces2() {
17390
+ return listExternalInterfaces();
17391
+ }
17209
17392
  function text(content) {
17210
17393
  return { content: [{ type: "text", text: content }] };
17211
17394
  }
@@ -18172,7 +18355,36 @@ NOTICE:
18172
18355
  }
18173
18356
  }
18174
18357
  );
18358
+ reg(
18359
+ server,
18360
+ "sdd_list_external_interfaces",
18361
+ {
18362
+ description: "List the bound project's consumable external surfaces (parent family, siblings, foreign imports) as discovery entries with origin, provenance, and freshness \u2014 the tool an agent inside a subproject uses to SEE its outward world instead of discovering it by failed reference resolution. Full contracts stay in the vendored snapshots (.wai/surfaces/); each entry summarizes the interface ids it exposes."
18363
+ },
18364
+ () => {
18365
+ try {
18366
+ return json(listExternalInterfaces2());
18367
+ } catch (e) {
18368
+ return errText(String(e));
18369
+ }
18370
+ }
18371
+ );
18175
18372
  registerSkillResources(server);
18373
+ try {
18374
+ const chainingParent = resolveChainingParent();
18375
+ if (chainingParent) {
18376
+ let externalSurfaceCount = 0;
18377
+ try {
18378
+ externalSurfaceCount = listExternalInterfaces2().length;
18379
+ } catch {
18380
+ }
18381
+ process.stderr.write(
18382
+ `[wairon mcp] chained subproject: this root is mounted as subsystem "${chainingParent.subsystemId}" of the parent project at ${chainingParent.parentRoot} \u2014 ${externalSurfaceCount} vendored external surface(s) discoverable via sdd_list_external_interfaces
18383
+ `
18384
+ );
18385
+ }
18386
+ } catch {
18387
+ }
18176
18388
  if (options.hostedTools) {
18177
18389
  const hostedStub = () => errText("This hosted tool is dispatched by the hosting data plane before reaching the MCP server; it is unavailable outside a hosted request.");
18178
18390
  reg(server, "sdd_host_lock_project", {
@@ -18308,6 +18520,8 @@ var init_server = __esm({
18308
18520
  init_narrative_labels();
18309
18521
  init_specs();
18310
18522
  init_skills();
18523
+ init_specs2();
18524
+ init_surfaces();
18311
18525
  SERVER_BUILD_STAMP = captureBuildStamp(__filename);
18312
18526
  STALE_SERVER_WARNING = "\n\n\u26A0 STALE SERVER: the wairon build on disk changed after this MCP server started. Restart the MCP session (e.g. /mcp reconnect) before further spec edits \u2014 writes through a stale server can silently drop fields introduced by newer schemas.";
18313
18527
  SKILL_RESOURCE_MIME = "text/markdown";
@@ -21381,7 +21595,7 @@ var require_dist = __commonJS({
21381
21595
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
21382
21596
  }
21383
21597
  var fs51 = __toESM2(require("fs"));
21384
- var path62 = __toESM2(require("path"));
21598
+ var path61 = __toESM2(require("path"));
21385
21599
  var import_fflate = require_node();
21386
21600
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
21387
21601
  function listEntries(archive) {
@@ -21419,8 +21633,8 @@ var require_dist = __commonJS({
21419
21633
  }
21420
21634
  function writeTree(destDir, files) {
21421
21635
  for (const file of files) {
21422
- const absolute = path62.join(destDir, file.path);
21423
- fs51.mkdirSync(path62.dirname(absolute), { recursive: true });
21636
+ const absolute = path61.join(destDir, file.path);
21637
+ fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
21424
21638
  fs51.writeFileSync(absolute, file.contents);
21425
21639
  }
21426
21640
  }
@@ -21433,10 +21647,10 @@ var require_dist = __commonJS({
21433
21647
  for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
21434
21648
  if (entry.isDirectory()) {
21435
21649
  if (SKIP_DIRS.has(entry.name)) continue;
21436
- walkPackDir(root, path62.join(current2, entry.name), out);
21650
+ walkPackDir(root, path61.join(current2, entry.name), out);
21437
21651
  } else if (entry.isFile()) {
21438
- const absolute = path62.join(current2, entry.name);
21439
- const relative22 = path62.relative(root, absolute).split(path62.sep).join("/");
21652
+ const absolute = path61.join(current2, entry.name);
21653
+ const relative22 = path61.relative(root, absolute).split(path61.sep).join("/");
21440
21654
  out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
21441
21655
  }
21442
21656
  }
@@ -22847,86 +23061,216 @@ async function generateLayer(options = {}) {
22847
23061
  }
22848
23062
 
22849
23063
  // src/commands/lock.ts
23064
+ var os7 = __toESM(require("os"));
22850
23065
  var import_inquirer2 = __toESM(require("inquirer"));
22851
23066
  init_logger();
22852
- init_loader();
22853
- init_fs();
22854
- init_validation();
22855
- init_specs2();
22856
- async function runLock(options = {}) {
22857
- assertProjectInitialized();
22858
- if (!pathExists(AI_PATHS.specsSystem())) {
22859
- logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
22860
- process.exit(1);
23067
+ init_defaults();
23068
+
23069
+ // src/core/detection.ts
23070
+ var fs18 = __toESM(require("fs"));
23071
+ var path27 = __toESM(require("path"));
23072
+ init_defaults();
23073
+ var PACKAGE_MARKERS = [
23074
+ "package.json",
23075
+ "pyproject.toml",
23076
+ "Cargo.toml",
23077
+ "go.mod",
23078
+ "build.gradle",
23079
+ "build.gradle.kts",
23080
+ "pom.xml"
23081
+ ];
23082
+ var MAX_SCAN_DEPTH = 5;
23083
+ function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23084
+ const candidates = /* @__PURE__ */ new Map();
23085
+ for (const c of detectGitSubmodules(projectRoot2)) {
23086
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22861
23087
  }
22862
- const projectConfig = loadProjectConfig();
22863
- logger.info("Analyzing and validating specifications in-memory...");
22864
- const index = scanAllSpecs({ recursive: options.recursive ?? true });
22865
- const promotable = collectPromotableSpecs(options.subsystem);
22866
- const originalStatuses = /* @__PURE__ */ new Map();
22867
- const isSpecInSubsystemScope = (specSubsystem) => {
22868
- if (!options.subsystem) return true;
22869
- if (!specSubsystem) return false;
22870
- return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
22871
- };
22872
- for (const s of index.subsystems) {
22873
- if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
22874
- originalStatuses.set(s, s.status);
22875
- s.status = "complete";
23088
+ for (const c of detectNestedGitRepos(projectRoot2)) {
23089
+ if (!candidates.has(c.path)) {
23090
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22876
23091
  }
22877
23092
  }
22878
- for (const c of index.components) {
22879
- if (isSpecInSubsystemScope(c.subsystem)) {
22880
- originalStatuses.set(c, c.status);
22881
- c.status = "complete";
22882
- }
23093
+ const gitPaths = new Set(
23094
+ Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23095
+ );
23096
+ for (const c of detectPackageRoots(projectRoot2)) {
23097
+ if (candidates.has(c.path)) continue;
23098
+ const insideGit = Array.from(gitPaths).some(
23099
+ (gp) => c.path === gp || c.path.startsWith(gp + "/")
23100
+ );
23101
+ if (insideGit) continue;
23102
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22883
23103
  }
22884
- for (const i of index.interfaces) {
22885
- const comp = index.components.find((c) => c.id === i.component);
22886
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22887
- originalStatuses.set(i, i.status);
22888
- i.status = "complete";
22889
- }
23104
+ const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23105
+ return deduplicateIds(sorted, alreadyTrackedIds);
23106
+ }
23107
+ function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23108
+ const idCount = /* @__PURE__ */ new Map();
23109
+ for (const id of existingIds) {
23110
+ idCount.set(id, (idCount.get(id) ?? 0) + 1);
22890
23111
  }
22891
- for (const m of index.implementations) {
22892
- const intf = index.interfaces.find((i) => i.id === m.contract);
22893
- const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
22894
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22895
- originalStatuses.set(m, m.status);
22896
- m.status = "complete";
22897
- }
23112
+ for (const c of candidates) {
23113
+ idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
22898
23114
  }
22899
- const dry = validateSddTree({
22900
- rules: projectConfig.rules,
22901
- projectType: projectConfig.projectType,
22902
- scopeSubsystem: options.subsystem,
22903
- recursive: options.recursive ?? true
23115
+ return candidates.map((c) => {
23116
+ if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23117
+ const parts = c.path.split("/");
23118
+ const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23119
+ return { ...c, suggestedId: qualifiedId2 };
22904
23120
  });
22905
- for (const [spec, status2] of originalStatuses.entries()) {
22906
- spec.status = status2;
23121
+ }
23122
+ function parseGitmodules(filePath) {
23123
+ const content = fs18.readFileSync(filePath, "utf-8");
23124
+ const entries = [];
23125
+ let current2 = {};
23126
+ for (const line2 of content.split("\n")) {
23127
+ const trimmed = line2.trim();
23128
+ const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23129
+ if (headerMatch) {
23130
+ if (current2.path) entries.push(current2);
23131
+ current2 = { name: headerMatch[1] };
23132
+ continue;
23133
+ }
23134
+ const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23135
+ if (keyVal) {
23136
+ const [, key, value] = keyVal;
23137
+ if (key === "path") current2.path = value.trim();
23138
+ if (key === "url") current2.url = value.trim();
23139
+ }
22907
23140
  }
22908
- const errors = dry.issues.filter((i) => i.severity === "error");
22909
- if (errors.length > 0) {
22910
- logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
22911
- let errorCount = 0;
22912
- const MAX_PRINT = 100;
22913
- let skippedErrors = 0;
22914
- for (const i of errors) {
22915
- if (errorCount < MAX_PRINT) {
22916
- logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
22917
- errorCount++;
22918
- } else {
22919
- skippedErrors++;
22920
- }
23141
+ if (current2.path) entries.push(current2);
23142
+ return entries;
23143
+ }
23144
+ function detectGitSubmodules(projectRoot2) {
23145
+ const gitmodulesPath = path27.join(projectRoot2, ".gitmodules");
23146
+ if (!fs18.existsSync(gitmodulesPath)) return [];
23147
+ return parseGitmodules(gitmodulesPath).map((entry) => ({
23148
+ suggestedId: pathToId(entry.path),
23149
+ suggestedName: pathToName(entry.path),
23150
+ path: normalizePath3(entry.path),
23151
+ type: "git-submodule",
23152
+ alreadyTracked: false
23153
+ }));
23154
+ }
23155
+ function detectNestedGitRepos(projectRoot2) {
23156
+ const results = [];
23157
+ walkForGit(projectRoot2, projectRoot2, 0, results);
23158
+ return results;
23159
+ }
23160
+ function walkForGit(projectRoot2, currentDir, depth, results) {
23161
+ if (depth > MAX_SCAN_DEPTH) return;
23162
+ let entries;
23163
+ try {
23164
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23165
+ } catch {
23166
+ return;
23167
+ }
23168
+ for (const entry of entries) {
23169
+ if (!entry.isDirectory()) continue;
23170
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23171
+ const fullPath = path27.join(currentDir, entry.name);
23172
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23173
+ if (relPath === "" || relPath === ".") continue;
23174
+ const gitPath = path27.join(fullPath, ".git");
23175
+ if (fs18.existsSync(gitPath)) {
23176
+ results.push({
23177
+ suggestedId: pathToId(relPath),
23178
+ suggestedName: pathToName(relPath),
23179
+ path: relPath,
23180
+ type: "git-repo",
23181
+ alreadyTracked: false
23182
+ });
23183
+ continue;
22921
23184
  }
22922
- if (skippedErrors > 0) {
22923
- logger.error(`... and ${skippedErrors} more error(s) omitted.`);
23185
+ walkForGit(projectRoot2, fullPath, depth + 1, results);
23186
+ }
23187
+ }
23188
+ function detectPackageRoots(projectRoot2) {
23189
+ const results = [];
23190
+ walkForPackages(projectRoot2, projectRoot2, 0, results);
23191
+ return results;
23192
+ }
23193
+ function walkForPackages(projectRoot2, currentDir, depth, results) {
23194
+ if (depth > MAX_SCAN_DEPTH) return;
23195
+ let entries;
23196
+ try {
23197
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23198
+ } catch {
23199
+ return;
23200
+ }
23201
+ for (const entry of entries) {
23202
+ if (!entry.isDirectory()) continue;
23203
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23204
+ const fullPath = path27.join(currentDir, entry.name);
23205
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23206
+ if (relPath === "" || relPath === ".") continue;
23207
+ const hasMarker = PACKAGE_MARKERS.some((m) => fs18.existsSync(path27.join(fullPath, m)));
23208
+ if (hasMarker) {
23209
+ results.push({
23210
+ suggestedId: pathToId(relPath),
23211
+ suggestedName: pathToName(relPath),
23212
+ path: relPath,
23213
+ type: "package-root",
23214
+ alreadyTracked: false
23215
+ });
22924
23216
  }
22925
- logger.blank();
22926
- logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
22927
- process.exit(1);
23217
+ walkForPackages(projectRoot2, fullPath, depth + 1, results);
22928
23218
  }
22929
- logger.header("Lock SDD specs");
23219
+ }
23220
+ function pathToId(relPath) {
23221
+ const basename11 = path27.basename(relPath);
23222
+ return basename11.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23223
+ }
23224
+ function pathToName(relPath) {
23225
+ const id = pathToId(relPath);
23226
+ return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23227
+ }
23228
+ function normalizePath3(p) {
23229
+ return p.replace(/\\/g, "/");
23230
+ }
23231
+
23232
+ // src/core/index.ts
23233
+ init_domains();
23234
+ init_validation();
23235
+ init_extensions();
23236
+ init_variants();
23237
+ init_rules();
23238
+ init_specs2();
23239
+ init_provision();
23240
+ init_diagram();
23241
+
23242
+ // src/core/lockfile.ts
23243
+ var fs19 = __toESM(require("fs"));
23244
+ var path28 = __toESM(require("path"));
23245
+ init_fs();
23246
+ function lockPath() {
23247
+ return aiDir("lock.json");
23248
+ }
23249
+ function readLockRecord() {
23250
+ try {
23251
+ return JSON.parse(fs19.readFileSync(lockPath(), "utf8"));
23252
+ } catch {
23253
+ return null;
23254
+ }
23255
+ }
23256
+ function writeLockRecord(record2) {
23257
+ const p = lockPath();
23258
+ fs19.mkdirSync(path28.dirname(p), { recursive: true });
23259
+ const tmp = `${p}.tmp`;
23260
+ fs19.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
23261
+ fs19.renameSync(tmp, p);
23262
+ }
23263
+
23264
+ // src/core/index.ts
23265
+ init_statehash();
23266
+ init_agent_resolver();
23267
+ init_skills();
23268
+ init_surfaces();
23269
+ init_openapi();
23270
+
23271
+ // src/commands/lock.ts
23272
+ async function runLock(options = {}, gate) {
23273
+ const promotable = collectPromotableSpecs(options.subsystem);
22930
23274
  if (promotable.length === 0) {
22931
23275
  logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
22932
23276
  } else {
@@ -22950,23 +23294,36 @@ async function runLock(options = {}) {
22950
23294
  default: false
22951
23295
  }
22952
23296
  ]);
22953
- if (!confirmed) {
22954
- logger.info("Cancelled. Nothing was changed.");
22955
- return;
22956
- }
23297
+ if (!confirmed) return null;
23298
+ }
23299
+ if (options.subsystem) {
23300
+ for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
23301
+ invalidateSpecCache();
23302
+ } else {
23303
+ promoteAllComplete();
22957
23304
  }
22958
- for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
22959
- invalidateSpecCache();
22960
23305
  if (promotable.length > 0) {
22961
23306
  logger.success(`Locked ${promotable.length} spec(s) as complete.`);
22962
23307
  }
22963
- logger.blank();
22964
- await runGenerate({ domain: options.subsystem });
22965
- logger.blank();
22966
- logger.success("Specs locked and agent topology generated.");
22967
- logger.warn(
22968
- "Restart any running AI agent sessions (Claude Code / Antigravity / Codex) so the newly generated implementer agents load \u2014 they are not picked up mid-session."
22969
- );
23308
+ let lockedBy = "local";
23309
+ try {
23310
+ lockedBy = `local:${os7.userInfo().username}`;
23311
+ } catch {
23312
+ }
23313
+ const record2 = {
23314
+ stateId: computeStateId(),
23315
+ lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
23316
+ lockedBy,
23317
+ validatorVersion: WAIRON_VERSION,
23318
+ validationResult: {
23319
+ valid: true,
23320
+ errors: 0,
23321
+ warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0
23322
+ },
23323
+ status: "ready"
23324
+ };
23325
+ writeLockRecord(record2);
23326
+ return record2;
22970
23327
  }
22971
23328
 
22972
23329
  // src/commands/validate.ts
@@ -23105,6 +23462,10 @@ async function runValidate(options = {}) {
23105
23462
  }
23106
23463
  }
23107
23464
 
23465
+ // src/cli/index.ts
23466
+ init_loader();
23467
+ init_fs();
23468
+
23108
23469
  // src/commands/list.ts
23109
23470
  var import_chalk7 = __toESM(require("chalk"));
23110
23471
  init_logger();
@@ -23219,9 +23580,9 @@ init_mcp();
23219
23580
  // src/commands/update.ts
23220
23581
  var https = __toESM(require("https"));
23221
23582
  var http = __toESM(require("http"));
23222
- var fs18 = __toESM(require("fs"));
23223
- var path27 = __toESM(require("path"));
23224
- var os7 = __toESM(require("os"));
23583
+ var fs20 = __toESM(require("fs"));
23584
+ var path29 = __toESM(require("path"));
23585
+ var os8 = __toESM(require("os"));
23225
23586
  var crypto2 = __toESM(require("crypto"));
23226
23587
  var import_child_process2 = require("child_process");
23227
23588
  init_logger();
@@ -23284,8 +23645,8 @@ async function runUpdate(options = {}) {
23284
23645
  logger.info(`Download manually from: ${release.html_url}`);
23285
23646
  process.exit(1);
23286
23647
  }
23287
- const tmpDir = os7.tmpdir();
23288
- const tmpFile = path27.join(tmpDir, assetName);
23648
+ const tmpDir = os8.tmpdir();
23649
+ const tmpFile = path29.join(tmpDir, assetName);
23289
23650
  logger.info(`Downloading ${assetName}...`);
23290
23651
  try {
23291
23652
  await downloadFile(asset.browser_download_url, tmpFile);
@@ -23302,16 +23663,16 @@ async function runUpdate(options = {}) {
23302
23663
  const checksumAssetName = assetName + ".sha256";
23303
23664
  const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
23304
23665
  if (checksumAsset) {
23305
- const tmpChecksum = path27.join(tmpDir, checksumAssetName);
23666
+ const tmpChecksum = path29.join(tmpDir, checksumAssetName);
23306
23667
  logger.info(`Verifying checksum...`);
23307
23668
  try {
23308
23669
  await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
23309
23670
  verifyChecksum(tmpFile, tmpChecksum, assetName);
23310
- fs18.unlinkSync(tmpChecksum);
23671
+ fs20.unlinkSync(tmpChecksum);
23311
23672
  } catch (err) {
23312
23673
  logger.error(`Checksum verification failed: ${err.message}`);
23313
23674
  try {
23314
- fs18.unlinkSync(tmpFile);
23675
+ fs20.unlinkSync(tmpFile);
23315
23676
  } catch {
23316
23677
  }
23317
23678
  process.exit(1);
@@ -23377,7 +23738,7 @@ function fetchReleases(repo) {
23377
23738
  }
23378
23739
  function downloadFile(url, dest) {
23379
23740
  return new Promise((resolve24, reject) => {
23380
- const file = fs18.createWriteStream(dest);
23741
+ const file = fs20.createWriteStream(dest);
23381
23742
  const get3 = url.startsWith("https://") ? https.get : http.get;
23382
23743
  get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
23383
23744
  if (res.statusCode === 301 || res.statusCode === 302) {
@@ -23399,21 +23760,21 @@ function downloadFile(url, dest) {
23399
23760
  });
23400
23761
  file.on("error", (err) => {
23401
23762
  res.destroy();
23402
- fs18.unlink(dest, () => {
23763
+ fs20.unlink(dest, () => {
23403
23764
  });
23404
23765
  reject(err);
23405
23766
  });
23406
23767
  }).on("error", (err) => {
23407
- fs18.unlink(dest, () => {
23768
+ fs20.unlink(dest, () => {
23408
23769
  });
23409
23770
  reject(err);
23410
23771
  });
23411
23772
  });
23412
23773
  }
23413
23774
  function verifyChecksum(filePath, checksumFile, expectedFilename) {
23414
- const checksumContent = fs18.readFileSync(checksumFile, "utf-8").trim();
23775
+ const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
23415
23776
  const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
23416
- const fileBuffer = fs18.readFileSync(filePath);
23777
+ const fileBuffer = fs20.readFileSync(filePath);
23417
23778
  const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
23418
23779
  if (actualHash !== expectedHash) {
23419
23780
  throw new Error(
@@ -23444,9 +23805,9 @@ function isPkgBinary2() {
23444
23805
  function installBinary(tmpFile, destPath) {
23445
23806
  const platform = process.platform;
23446
23807
  const isZip = tmpFile.endsWith(".zip");
23447
- const extractDir = path27.join(os7.tmpdir(), "wairon-extract");
23448
- if (fs18.existsSync(extractDir)) fs18.rmSync(extractDir, { recursive: true });
23449
- fs18.mkdirSync(extractDir, { recursive: true });
23808
+ const extractDir = path29.join(os8.tmpdir(), "wairon-extract");
23809
+ if (fs20.existsSync(extractDir)) fs20.rmSync(extractDir, { recursive: true });
23810
+ fs20.mkdirSync(extractDir, { recursive: true });
23450
23811
  if (isZip) {
23451
23812
  (0, import_child_process2.execSync)(
23452
23813
  `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
@@ -23456,18 +23817,18 @@ function installBinary(tmpFile, destPath) {
23456
23817
  (0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
23457
23818
  }
23458
23819
  const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
23459
- const extractedBinary = path27.join(extractDir, binaryName);
23460
- if (!fs18.existsSync(extractedBinary)) {
23820
+ const extractedBinary = path29.join(extractDir, binaryName);
23821
+ if (!fs20.existsSync(extractedBinary)) {
23461
23822
  throw new Error(`Extracted binary not found at ${extractedBinary}`);
23462
23823
  }
23463
23824
  if (platform === "win32") {
23464
23825
  const oldPath = destPath + ".old";
23465
23826
  try {
23466
23827
  cleanStaleBinary(oldPath);
23467
- fs18.renameSync(destPath, oldPath);
23468
- fs18.copyFileSync(extractedBinary, destPath);
23828
+ fs20.renameSync(destPath, oldPath);
23829
+ fs20.copyFileSync(extractedBinary, destPath);
23469
23830
  try {
23470
- fs18.unlinkSync(oldPath);
23831
+ fs20.unlinkSync(oldPath);
23471
23832
  } catch {
23472
23833
  }
23473
23834
  } catch (err) {
@@ -23481,25 +23842,25 @@ function installBinary(tmpFile, destPath) {
23481
23842
  }
23482
23843
  } else {
23483
23844
  const tmpDest = destPath + ".new";
23484
- fs18.copyFileSync(extractedBinary, tmpDest);
23485
- fs18.chmodSync(tmpDest, 493);
23486
- fs18.renameSync(tmpDest, destPath);
23845
+ fs20.copyFileSync(extractedBinary, tmpDest);
23846
+ fs20.chmodSync(tmpDest, 493);
23847
+ fs20.renameSync(tmpDest, destPath);
23487
23848
  }
23488
23849
  try {
23489
- fs18.unlinkSync(tmpFile);
23850
+ fs20.unlinkSync(tmpFile);
23490
23851
  } catch {
23491
23852
  }
23492
23853
  try {
23493
- fs18.rmSync(extractDir, { recursive: true });
23854
+ fs20.rmSync(extractDir, { recursive: true });
23494
23855
  } catch {
23495
23856
  }
23496
23857
  }
23497
23858
  function cleanStaleBinary(oldPath) {
23498
23859
  const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
23499
23860
  if (!target) return;
23500
- if (fs18.existsSync(target)) {
23861
+ if (fs20.existsSync(target)) {
23501
23862
  try {
23502
- fs18.unlinkSync(target);
23863
+ fs20.unlinkSync(target);
23503
23864
  } catch {
23504
23865
  }
23505
23866
  }
@@ -23745,171 +24106,6 @@ async function filteredCheckbox(config) {
23745
24106
 
23746
24107
  // src/commands/domains.ts
23747
24108
  init_loader();
23748
-
23749
- // src/core/detection.ts
23750
- var fs19 = __toESM(require("fs"));
23751
- var path28 = __toESM(require("path"));
23752
- init_defaults();
23753
- var PACKAGE_MARKERS = [
23754
- "package.json",
23755
- "pyproject.toml",
23756
- "Cargo.toml",
23757
- "go.mod",
23758
- "build.gradle",
23759
- "build.gradle.kts",
23760
- "pom.xml"
23761
- ];
23762
- var MAX_SCAN_DEPTH = 5;
23763
- function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23764
- const candidates = /* @__PURE__ */ new Map();
23765
- for (const c of detectGitSubmodules(projectRoot2)) {
23766
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23767
- }
23768
- for (const c of detectNestedGitRepos(projectRoot2)) {
23769
- if (!candidates.has(c.path)) {
23770
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23771
- }
23772
- }
23773
- const gitPaths = new Set(
23774
- Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23775
- );
23776
- for (const c of detectPackageRoots(projectRoot2)) {
23777
- if (candidates.has(c.path)) continue;
23778
- const insideGit = Array.from(gitPaths).some(
23779
- (gp) => c.path === gp || c.path.startsWith(gp + "/")
23780
- );
23781
- if (insideGit) continue;
23782
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23783
- }
23784
- const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23785
- return deduplicateIds(sorted, alreadyTrackedIds);
23786
- }
23787
- function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23788
- const idCount = /* @__PURE__ */ new Map();
23789
- for (const id of existingIds) {
23790
- idCount.set(id, (idCount.get(id) ?? 0) + 1);
23791
- }
23792
- for (const c of candidates) {
23793
- idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
23794
- }
23795
- return candidates.map((c) => {
23796
- if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23797
- const parts = c.path.split("/");
23798
- const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23799
- return { ...c, suggestedId: qualifiedId2 };
23800
- });
23801
- }
23802
- function parseGitmodules(filePath) {
23803
- const content = fs19.readFileSync(filePath, "utf-8");
23804
- const entries = [];
23805
- let current2 = {};
23806
- for (const line2 of content.split("\n")) {
23807
- const trimmed = line2.trim();
23808
- const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23809
- if (headerMatch) {
23810
- if (current2.path) entries.push(current2);
23811
- current2 = { name: headerMatch[1] };
23812
- continue;
23813
- }
23814
- const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23815
- if (keyVal) {
23816
- const [, key, value] = keyVal;
23817
- if (key === "path") current2.path = value.trim();
23818
- if (key === "url") current2.url = value.trim();
23819
- }
23820
- }
23821
- if (current2.path) entries.push(current2);
23822
- return entries;
23823
- }
23824
- function detectGitSubmodules(projectRoot2) {
23825
- const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
23826
- if (!fs19.existsSync(gitmodulesPath)) return [];
23827
- return parseGitmodules(gitmodulesPath).map((entry) => ({
23828
- suggestedId: pathToId(entry.path),
23829
- suggestedName: pathToName(entry.path),
23830
- path: normalizePath3(entry.path),
23831
- type: "git-submodule",
23832
- alreadyTracked: false
23833
- }));
23834
- }
23835
- function detectNestedGitRepos(projectRoot2) {
23836
- const results = [];
23837
- walkForGit(projectRoot2, projectRoot2, 0, results);
23838
- return results;
23839
- }
23840
- function walkForGit(projectRoot2, currentDir, depth, results) {
23841
- if (depth > MAX_SCAN_DEPTH) return;
23842
- let entries;
23843
- try {
23844
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23845
- } catch {
23846
- return;
23847
- }
23848
- for (const entry of entries) {
23849
- if (!entry.isDirectory()) continue;
23850
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23851
- const fullPath = path28.join(currentDir, entry.name);
23852
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23853
- if (relPath === "" || relPath === ".") continue;
23854
- const gitPath = path28.join(fullPath, ".git");
23855
- if (fs19.existsSync(gitPath)) {
23856
- results.push({
23857
- suggestedId: pathToId(relPath),
23858
- suggestedName: pathToName(relPath),
23859
- path: relPath,
23860
- type: "git-repo",
23861
- alreadyTracked: false
23862
- });
23863
- continue;
23864
- }
23865
- walkForGit(projectRoot2, fullPath, depth + 1, results);
23866
- }
23867
- }
23868
- function detectPackageRoots(projectRoot2) {
23869
- const results = [];
23870
- walkForPackages(projectRoot2, projectRoot2, 0, results);
23871
- return results;
23872
- }
23873
- function walkForPackages(projectRoot2, currentDir, depth, results) {
23874
- if (depth > MAX_SCAN_DEPTH) return;
23875
- let entries;
23876
- try {
23877
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23878
- } catch {
23879
- return;
23880
- }
23881
- for (const entry of entries) {
23882
- if (!entry.isDirectory()) continue;
23883
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23884
- const fullPath = path28.join(currentDir, entry.name);
23885
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23886
- if (relPath === "" || relPath === ".") continue;
23887
- const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
23888
- if (hasMarker) {
23889
- results.push({
23890
- suggestedId: pathToId(relPath),
23891
- suggestedName: pathToName(relPath),
23892
- path: relPath,
23893
- type: "package-root",
23894
- alreadyTracked: false
23895
- });
23896
- }
23897
- walkForPackages(projectRoot2, fullPath, depth + 1, results);
23898
- }
23899
- }
23900
- function pathToId(relPath) {
23901
- const basename12 = path28.basename(relPath);
23902
- return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23903
- }
23904
- function pathToName(relPath) {
23905
- const id = pathToId(relPath);
23906
- return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23907
- }
23908
- function normalizePath3(p) {
23909
- return p.replace(/\\/g, "/");
23910
- }
23911
-
23912
- // src/commands/domains.ts
23913
24109
  init_domains();
23914
24110
  init_domain();
23915
24111
  async function runDomainsList() {
@@ -24099,9 +24295,9 @@ async function runSkillsInstall() {
24099
24295
  }
24100
24296
 
24101
24297
  // src/commands/doctor.ts
24102
- var fs20 = __toESM(require("fs"));
24103
- var os8 = __toESM(require("os"));
24104
- var path29 = __toESM(require("path"));
24298
+ var fs21 = __toESM(require("fs"));
24299
+ var os9 = __toESM(require("os"));
24300
+ var path30 = __toESM(require("path"));
24105
24301
  var import_chalk12 = __toESM(require("chalk"));
24106
24302
  init_logger();
24107
24303
  init_defaults();
@@ -24131,10 +24327,10 @@ function stampVerdict(content) {
24131
24327
  return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
24132
24328
  }
24133
24329
  function mcpEntryHealth(settingsPath) {
24134
- if (!fs20.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
24330
+ if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
24135
24331
  let entry;
24136
24332
  try {
24137
- const s = JSON.parse(fs20.readFileSync(settingsPath, "utf8"));
24333
+ const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
24138
24334
  entry = s.mcpServers?.["wairon"];
24139
24335
  } catch {
24140
24336
  return { mark: "error", note: "parse error" };
@@ -24142,7 +24338,7 @@ function mcpEntryHealth(settingsPath) {
24142
24338
  if (!entry) return { mark: "warn", note: "not registered" };
24143
24339
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
24144
24340
  const scriptPath = entry.args[0];
24145
- if (!fs20.existsSync(scriptPath)) {
24341
+ if (!fs21.existsSync(scriptPath)) {
24146
24342
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
24147
24343
  }
24148
24344
  }
@@ -24194,7 +24390,7 @@ async function runDoctor(options = {}) {
24194
24390
  const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
24195
24391
  const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
24196
24392
  if (missing.length > 0) {
24197
- line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) => path29.relative(getProjectRoot(), d) || ".").join(", ")}. Run \`wairon doctor --fix\` to initialize them.`);
24393
+ line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) => path30.relative(getProjectRoot(), d) || ".").join(", ")}. Run \`wairon doctor --fix\` to initialize them.`);
24198
24394
  }
24199
24395
  } catch {
24200
24396
  }
@@ -24232,7 +24428,7 @@ async function runDoctor(options = {}) {
24232
24428
  const gp = localGuideFilePath(process.cwd(), t);
24233
24429
  if (!gp || seenGuides.has(gp)) continue;
24234
24430
  seenGuides.add(gp);
24235
- const rel2 = path29.relative(process.cwd(), gp).replace(/\\/g, "/");
24431
+ const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
24236
24432
  if (!pathExists(gp)) {
24237
24433
  line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
24238
24434
  continue;
@@ -24266,17 +24462,17 @@ async function runDoctor(options = {}) {
24266
24462
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
24267
24463
  }
24268
24464
  if (wantGemini) {
24269
- const globalCfg = path29.join(os8.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
24465
+ const globalCfg = path30.join(os9.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
24270
24466
  const hg = mcpEntryHealth(globalCfg);
24271
24467
  line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
24272
24468
  const projPath = fromProjectRoot(".gemini", "settings.json");
24273
- if (fs20.existsSync(projPath)) {
24469
+ if (fs21.existsSync(projPath)) {
24274
24470
  const hp = mcpEntryHealth(projPath);
24275
24471
  line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
24276
24472
  }
24277
24473
  }
24278
- const pluginDir = path29.join(os8.homedir(), ".gemini", "config", "plugins", "wairon");
24279
- if (fs20.existsSync(pluginDir)) {
24474
+ const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
24475
+ if (fs21.existsSync(pluginDir)) {
24280
24476
  line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
24281
24477
  }
24282
24478
  logger.blank();
@@ -24307,7 +24503,7 @@ async function applyFixes() {
24307
24503
  const legacySpecs = findLegacySpecFiles();
24308
24504
  if (legacySpecs.length > 0) {
24309
24505
  for (const { path: oldPath, expected: newPath } of legacySpecs) {
24310
- fs20.renameSync(oldPath, newPath);
24506
+ fs21.renameSync(oldPath, newPath);
24311
24507
  }
24312
24508
  console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
24313
24509
  }
@@ -24361,8 +24557,8 @@ function printSummary(tally) {
24361
24557
  }
24362
24558
 
24363
24559
  // src/commands/diagram.ts
24364
- var fs21 = __toESM(require("fs"));
24365
- var path30 = __toESM(require("path"));
24560
+ var fs22 = __toESM(require("fs"));
24561
+ var path31 = __toESM(require("path"));
24366
24562
  init_logger();
24367
24563
  init_loader();
24368
24564
  init_fs();
@@ -24397,8 +24593,8 @@ function collectIssues() {
24397
24593
  }
24398
24594
  function writeCanvas(dest) {
24399
24595
  const model = buildCanvasModel(collectIssues());
24400
- ensureDir(path30.dirname(path30.resolve(dest)));
24401
- fs21.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
24596
+ ensureDir(path31.dirname(path31.resolve(dest)));
24597
+ fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
24402
24598
  }
24403
24599
  function parseSequenceRef(ref) {
24404
24600
  const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
@@ -24413,55 +24609,55 @@ async function runDiagram(rawOptions = {}) {
24413
24609
  assertProjectInitialized();
24414
24610
  const options = applyFormat(rawOptions);
24415
24611
  if (options.canvas && !options.all) {
24416
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24612
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24417
24613
  writeCanvas(dest2);
24418
24614
  logger.success(`Interactive canvas written to ${dest2}`);
24419
24615
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
24420
24616
  return;
24421
24617
  }
24422
24618
  if (options.drawio && !options.all) {
24423
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
24424
- ensureDir(path30.dirname(path30.resolve(dest2)));
24425
- fs21.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
24619
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
24620
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24621
+ fs22.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
24426
24622
  logger.success(`draw.io diagram written to ${dest2}`);
24427
24623
  logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
24428
24624
  return;
24429
24625
  }
24430
24626
  if (options.excalidraw && !options.all) {
24431
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
24432
- ensureDir(path30.dirname(path30.resolve(dest2)));
24433
- fs21.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
24627
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
24628
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24629
+ fs22.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
24434
24630
  logger.success(`Excalidraw scene written to ${dest2}`);
24435
24631
  logger.info("Open with excalidraw.com or the VS Code extension.");
24436
24632
  return;
24437
24633
  }
24438
24634
  const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
24439
24635
  if (!options.all && !options.sequence && !wantsMermaid) {
24440
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24636
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24441
24637
  writeCanvas(dest2);
24442
24638
  logger.success(`Interactive canvas written to ${dest2}`);
24443
24639
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
24444
24640
  return;
24445
24641
  }
24446
24642
  if (options.all) {
24447
- const outDir = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams");
24643
+ const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
24448
24644
  const files = generateDiagramSet();
24449
24645
  if (files.length === 0) {
24450
24646
  logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
24451
24647
  return;
24452
24648
  }
24453
24649
  for (const file of files) {
24454
- const dest2 = path30.join(outDir, file.relPath);
24455
- ensureDir(path30.dirname(dest2));
24456
- fs21.writeFileSync(dest2, toMarkdown(file), "utf-8");
24650
+ const dest2 = path31.join(outDir, file.relPath);
24651
+ ensureDir(path31.dirname(dest2));
24652
+ fs22.writeFileSync(dest2, toMarkdown(file), "utf-8");
24457
24653
  }
24458
- writeCanvas(path30.join(outDir, "canvas.html"));
24654
+ writeCanvas(path31.join(outDir, "canvas.html"));
24459
24655
  const exportModel = buildCanvasModel();
24460
- fs21.writeFileSync(path30.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
24461
- fs21.writeFileSync(path30.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
24656
+ fs22.writeFileSync(path31.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
24657
+ fs22.writeFileSync(path31.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
24462
24658
  const graph = loadSpecGraph();
24463
- const indexPath = path30.join(outDir, "README.md");
24464
- fs21.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
24659
+ const indexPath = path31.join(outDir, "README.md");
24660
+ fs22.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
24465
24661
  logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
24466
24662
  for (const file of files.slice(0, 12)) {
24467
24663
  logger.info(` ${file.relPath}`);
@@ -24472,26 +24668,26 @@ async function runDiagram(rawOptions = {}) {
24472
24668
  let mermaid;
24473
24669
  let title;
24474
24670
  let defaultDest;
24475
- const diagramsDir = path30.join(AI_PATHS.docsDir(), "diagrams");
24671
+ const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
24476
24672
  if (options.sequence) {
24477
24673
  const { component, method: method2 } = parseSequenceRef(options.sequence);
24478
24674
  mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
24479
24675
  title = `${component}.${method2} \u2014 narrative sequence`;
24480
- defaultDest = path30.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
24676
+ defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
24481
24677
  } else if (options.subsystem) {
24482
24678
  mermaid = generateComponentDiagram({ subsystem: options.subsystem });
24483
24679
  title = `${options.subsystem} \u2014 components`;
24484
- defaultDest = path30.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
24680
+ defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
24485
24681
  } else {
24486
24682
  mermaid = generateComponentDiagram();
24487
24683
  title = "Component architecture";
24488
- defaultDest = path30.join(diagramsDir, "system.md");
24684
+ defaultDest = path31.join(diagramsDir, "system.md");
24489
24685
  }
24490
24686
  const dest = options.out ?? defaultDest;
24491
- ensureDir(path30.dirname(path30.resolve(dest)));
24687
+ ensureDir(path31.dirname(path31.resolve(dest)));
24492
24688
  const content = dest.endsWith(".mmd") ? `${mermaid}
24493
24689
  ` : toMarkdown({ relPath: dest, title, mermaid });
24494
- fs21.writeFileSync(dest, content, "utf-8");
24690
+ fs22.writeFileSync(dest, content, "utf-8");
24495
24691
  logger.success(`Mermaid diagram written to ${dest}`);
24496
24692
  logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
24497
24693
  }
@@ -24600,8 +24796,8 @@ Component variants (${variants.length})
24600
24796
  }
24601
24797
 
24602
24798
  // src/commands/packs.ts
24603
- var fs22 = __toESM(require("fs"));
24604
- var path31 = __toESM(require("path"));
24799
+ var fs23 = __toESM(require("fs"));
24800
+ var path32 = __toESM(require("path"));
24605
24801
  var import_chalk16 = __toESM(require("chalk"));
24606
24802
  var import_sdk = __toESM(require_dist());
24607
24803
  init_logger();
@@ -24627,11 +24823,11 @@ function describe(probe2) {
24627
24823
  return parts.join(", ");
24628
24824
  }
24629
24825
  function resolveSourceUnit(source) {
24630
- const abs = path31.resolve(source);
24631
- if (!fs22.existsSync(abs)) {
24826
+ const abs = path32.resolve(source);
24827
+ if (!fs23.existsSync(abs)) {
24632
24828
  throw new Error(`Pack source "${source}" does not exist.`);
24633
24829
  }
24634
- const isDir = fs22.statSync(abs).isDirectory();
24830
+ const isDir = fs23.statSync(abs).isDirectory();
24635
24831
  if (isDir && !packDirEntry(abs)) {
24636
24832
  throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
24637
24833
  }
@@ -24647,7 +24843,7 @@ async function addPack(source, options = {}) {
24647
24843
  }
24648
24844
  const { abs } = resolveSourceUnit(source);
24649
24845
  const scope = options.global ? "global" : "project";
24650
- const probe2 = probePack(abs, path31.dirname(abs), scope);
24846
+ const probe2 = probePack(abs, path32.dirname(abs), scope);
24651
24847
  if (probe2.error) {
24652
24848
  logger.error(probe2.error);
24653
24849
  process.exitCode = 1;
@@ -24655,10 +24851,10 @@ async function addPack(source, options = {}) {
24655
24851
  }
24656
24852
  if (options.global) {
24657
24853
  const destDir = globalPacksDir();
24658
- const dest2 = path31.join(destDir, path31.basename(abs));
24659
- if (path31.resolve(dest2) !== abs) {
24660
- fs22.mkdirSync(destDir, { recursive: true });
24661
- fs22.cpSync(abs, dest2, { recursive: true, force: true });
24854
+ const dest2 = path32.join(destDir, path32.basename(abs));
24855
+ if (path32.resolve(dest2) !== abs) {
24856
+ fs23.mkdirSync(destDir, { recursive: true });
24857
+ fs23.cpSync(abs, dest2, { recursive: true, force: true });
24662
24858
  }
24663
24859
  logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
24664
24860
  logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
@@ -24671,11 +24867,11 @@ async function addPack(source, options = {}) {
24671
24867
  return;
24672
24868
  }
24673
24869
  const root = getProjectRoot();
24674
- const relRef = `.wai/packs/${path31.basename(abs)}`;
24675
- const dest = path31.join(root, ".wai", "packs", path31.basename(abs));
24676
- if (path31.resolve(dest) !== abs) {
24677
- fs22.mkdirSync(path31.dirname(dest), { recursive: true });
24678
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24870
+ const relRef = `.wai/packs/${path32.basename(abs)}`;
24871
+ const dest = path32.join(root, ".wai", "packs", path32.basename(abs));
24872
+ if (path32.resolve(dest) !== abs) {
24873
+ fs23.mkdirSync(path32.dirname(dest), { recursive: true });
24874
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
24679
24875
  }
24680
24876
  const config = loadProjectConfig();
24681
24877
  const packs = config.extensions?.packs ?? [];
@@ -24684,14 +24880,14 @@ async function addPack(source, options = {}) {
24684
24880
  saveProjectConfig(config);
24685
24881
  logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
24686
24882
  } else {
24687
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24883
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
24688
24884
  logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
24689
24885
  }
24690
24886
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
24691
24887
  }
24692
24888
  async function addPackFromArchive(source, options) {
24693
- const abs = path31.resolve(source);
24694
- if (!fs22.existsSync(abs) || !fs22.statSync(abs).isFile()) {
24889
+ const abs = path32.resolve(source);
24890
+ if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
24695
24891
  logger.error(`Pack archive "${source}" does not exist.`);
24696
24892
  process.exitCode = 1;
24697
24893
  return;
@@ -24706,27 +24902,27 @@ async function addPackFromArchive(source, options) {
24706
24902
  process.exitCode = 1;
24707
24903
  return;
24708
24904
  }
24709
- baseDir = path31.join(getProjectRoot(), ".wai", "packs");
24905
+ baseDir = path32.join(getProjectRoot(), ".wai", "packs");
24710
24906
  }
24711
- const bytes = fs22.readFileSync(abs);
24712
- fs22.mkdirSync(baseDir, { recursive: true });
24713
- const staging = fs22.mkdtempSync(path31.join(baseDir, ".wpack-staging-"));
24907
+ const bytes = fs23.readFileSync(abs);
24908
+ fs23.mkdirSync(baseDir, { recursive: true });
24909
+ const staging = fs23.mkdtempSync(path32.join(baseDir, ".wpack-staging-"));
24714
24910
  let result;
24715
24911
  try {
24716
24912
  result = (0, import_sdk.extractPack)(bytes, staging);
24717
24913
  } catch (err) {
24718
- fs22.rmSync(staging, { recursive: true, force: true });
24719
- logger.error(`Failed to extract pack archive "${path31.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
24914
+ fs23.rmSync(staging, { recursive: true, force: true });
24915
+ logger.error(`Failed to extract pack archive "${path32.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
24720
24916
  process.exitCode = 1;
24721
24917
  return;
24722
24918
  }
24723
24919
  const name = result.name;
24724
- const destDir = path31.join(baseDir, name);
24725
- if (fs22.existsSync(destDir)) fs22.rmSync(destDir, { recursive: true, force: true });
24726
- fs22.renameSync(staging, destDir);
24727
- const probe2 = probePack(destDir, path31.dirname(destDir), scope);
24920
+ const destDir = path32.join(baseDir, name);
24921
+ if (fs23.existsSync(destDir)) fs23.rmSync(destDir, { recursive: true, force: true });
24922
+ fs23.renameSync(staging, destDir);
24923
+ const probe2 = probePack(destDir, path32.dirname(destDir), scope);
24728
24924
  if (probe2.error) {
24729
- fs22.rmSync(destDir, { recursive: true, force: true });
24925
+ fs23.rmSync(destDir, { recursive: true, force: true });
24730
24926
  logger.error(probe2.error);
24731
24927
  process.exitCode = 1;
24732
24928
  return;
@@ -24744,7 +24940,7 @@ async function addPackFromArchive(source, options) {
24744
24940
  saveProjectConfig(config);
24745
24941
  logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
24746
24942
  } else {
24747
- logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path31.basename(abs)}.`);
24943
+ logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path32.basename(abs)}.`);
24748
24944
  }
24749
24945
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
24750
24946
  }
@@ -24765,7 +24961,7 @@ async function buildPack(source, options = {}) {
24765
24961
  const sourceDir = source && source.length > 0 ? source : ".";
24766
24962
  const result = (0, import_sdk.buildPack)(sourceDir);
24767
24963
  const outPath = options.out ?? result.suggestedFileName;
24768
- fs22.writeFileSync(outPath, result.archive);
24964
+ fs23.writeFileSync(outPath, result.archive);
24769
24965
  logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
24770
24966
  logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
24771
24967
  }
@@ -24779,9 +24975,9 @@ async function listPacks() {
24779
24975
  console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
24780
24976
  if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
24781
24977
  for (const ref of globalRefs) {
24782
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24783
- if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path31.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24784
- else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path31.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24978
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
24979
+ if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path32.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24980
+ else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path32.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24785
24981
  }
24786
24982
  console.log("");
24787
24983
  if (!inProject) {
@@ -24802,9 +24998,9 @@ async function listPacks() {
24802
24998
  async function removePack(name, options = {}) {
24803
24999
  if (options.global) {
24804
25000
  for (const ref of discoverPacks(globalPacksDir())) {
24805
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24806
- if (probe2.name === name || path31.basename(ref) === name) {
24807
- fs22.rmSync(ref, { recursive: true, force: true });
25001
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
25002
+ if (probe2.name === name || path32.basename(ref) === name) {
25003
+ fs23.rmSync(ref, { recursive: true, force: true });
24808
25004
  logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
24809
25005
  return;
24810
25006
  }
@@ -24823,16 +25019,16 @@ async function removePack(name, options = {}) {
24823
25019
  const packs = config.extensions?.packs ?? [];
24824
25020
  for (const ref of packs) {
24825
25021
  const probe2 = probePack(ref, root, "project");
24826
- if (probe2.name === name || ref === name || path31.basename(ref) === name) {
25022
+ if (probe2.name === name || ref === name || path32.basename(ref) === name) {
24827
25023
  config.extensions = {
24828
25024
  packs: packs.filter((p) => p !== ref),
24829
25025
  useGlobalPacks: config.extensions?.useGlobalPacks ?? true
24830
25026
  };
24831
25027
  saveProjectConfig(config);
24832
- const resolved = path31.resolve(root, ref);
24833
- const vendorDir = path31.resolve(root, ".wai", "packs");
24834
- if (resolved.startsWith(vendorDir + path31.sep)) {
24835
- fs22.rmSync(resolved, { recursive: true, force: true });
25028
+ const resolved = path32.resolve(root, ref);
25029
+ const vendorDir = path32.resolve(root, ".wai", "packs");
25030
+ if (resolved.startsWith(vendorDir + path32.sep)) {
25031
+ fs23.rmSync(resolved, { recursive: true, force: true });
24836
25032
  logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
24837
25033
  } else {
24838
25034
  logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
@@ -24846,8 +25042,8 @@ async function removePack(name, options = {}) {
24846
25042
 
24847
25043
  // src/commands/host.ts
24848
25044
  var fs50 = __toESM(require("fs"));
24849
- var path60 = __toESM(require("path"));
24850
- var os9 = __toESM(require("os"));
25045
+ var path59 = __toESM(require("path"));
25046
+ var os10 = __toESM(require("os"));
24851
25047
  var crypto20 = __toESM(require("crypto"));
24852
25048
  var import_child_process5 = require("child_process");
24853
25049
  var import_chalk17 = __toESM(require("chalk"));
@@ -24874,29 +25070,29 @@ var UNAUTHENTICATED = {
24874
25070
  var WEB_SESSION_PREFIX = "ws_";
24875
25071
 
24876
25072
  // src/server/credentials.ts
24877
- var fs23 = __toESM(require("fs"));
24878
- var path32 = __toESM(require("path"));
25073
+ var fs24 = __toESM(require("fs"));
25074
+ var path33 = __toESM(require("path"));
24879
25075
  var crypto3 = __toESM(require("crypto"));
24880
25076
  var HASH_NS = "wairon:token:v1";
24881
25077
  function hashToken(token) {
24882
25078
  return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
24883
25079
  }
24884
25080
  function storePath(dataDir) {
24885
- return path32.join(dataDir, "auth", "credentials.json");
25081
+ return path33.join(dataDir, "auth", "credentials.json");
24886
25082
  }
24887
25083
  function load3(dataDir) {
24888
25084
  try {
24889
- return JSON.parse(fs23.readFileSync(storePath(dataDir), "utf8"));
25085
+ return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
24890
25086
  } catch {
24891
25087
  return [];
24892
25088
  }
24893
25089
  }
24894
25090
  function save(dataDir, records) {
24895
25091
  const p = storePath(dataDir);
24896
- fs23.mkdirSync(path32.dirname(p), { recursive: true });
25092
+ fs24.mkdirSync(path33.dirname(p), { recursive: true });
24897
25093
  const tmp = `${p}.tmp`;
24898
- fs23.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24899
- fs23.renameSync(tmp, p);
25094
+ fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25095
+ fs24.renameSync(tmp, p);
24900
25096
  }
24901
25097
  function digestEquals(a, b) {
24902
25098
  const ab = Buffer.from(a, "hex");
@@ -24941,17 +25137,17 @@ function listByOwner(dataDir, ownerUserId) {
24941
25137
  }
24942
25138
 
24943
25139
  // src/server/websessions.ts
24944
- var fs24 = __toESM(require("fs"));
24945
- var path33 = __toESM(require("path"));
25140
+ var fs25 = __toESM(require("fs"));
25141
+ var path34 = __toESM(require("path"));
24946
25142
  var crypto4 = __toESM(require("crypto"));
24947
25143
  function storePath2(dataDir) {
24948
- return path33.join(dataDir, "web-sessions.json");
25144
+ return path34.join(dataDir, "web-sessions.json");
24949
25145
  }
24950
25146
  function readSessions(dataDir) {
24951
25147
  const p = storePath2(dataDir);
24952
25148
  let raw;
24953
25149
  try {
24954
- raw = fs24.readFileSync(p, "utf8");
25150
+ raw = fs25.readFileSync(p, "utf8");
24955
25151
  } catch (e) {
24956
25152
  if (e.code === "ENOENT") return [];
24957
25153
  throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
@@ -24966,10 +25162,10 @@ function readSessions(dataDir) {
24966
25162
  }
24967
25163
  function persistSessions(dataDir, sessions) {
24968
25164
  const p = storePath2(dataDir);
24969
- fs24.mkdirSync(path33.dirname(p), { recursive: true });
25165
+ fs25.mkdirSync(path34.dirname(p), { recursive: true });
24970
25166
  const tmp = `${p}.tmp`;
24971
- fs24.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
24972
- fs24.renameSync(tmp, p);
25167
+ fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
25168
+ fs25.renameSync(tmp, p);
24973
25169
  }
24974
25170
  function mintSessionId() {
24975
25171
  return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
@@ -25130,17 +25326,17 @@ function listWebSessionsBySubject(dataDir, userId) {
25130
25326
  }
25131
25327
 
25132
25328
  // src/server/users.ts
25133
- var fs25 = __toESM(require("fs"));
25134
- var path34 = __toESM(require("path"));
25329
+ var fs26 = __toESM(require("fs"));
25330
+ var path35 = __toESM(require("path"));
25135
25331
  var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
25136
25332
  function storePath3(dataDir) {
25137
- return path34.join(dataDir, "users.json");
25333
+ return path35.join(dataDir, "users.json");
25138
25334
  }
25139
25335
  function loadStore(dataDir) {
25140
25336
  const p = storePath3(dataDir);
25141
25337
  let raw;
25142
25338
  try {
25143
- raw = fs25.readFileSync(p, "utf8");
25339
+ raw = fs26.readFileSync(p, "utf8");
25144
25340
  } catch (err) {
25145
25341
  if (err.code === "ENOENT") return [];
25146
25342
  throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
@@ -25158,10 +25354,10 @@ function loadStore(dataDir) {
25158
25354
  }
25159
25355
  function replaceAll(dataDir, records) {
25160
25356
  const p = storePath3(dataDir);
25161
- fs25.mkdirSync(path34.dirname(p), { recursive: true });
25357
+ fs26.mkdirSync(path35.dirname(p), { recursive: true });
25162
25358
  const tmp = `${p}.tmp`;
25163
- fs25.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25164
- fs25.renameSync(tmp, p);
25359
+ fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25360
+ fs26.renameSync(tmp, p);
25165
25361
  }
25166
25362
  function registryUpsert(dataDir, record2) {
25167
25363
  const records = loadStore(dataDir);
@@ -25263,11 +25459,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
25263
25459
  }
25264
25460
 
25265
25461
  // src/server/instance.ts
25266
- var fs26 = __toESM(require("fs"));
25267
- var path35 = __toESM(require("path"));
25462
+ var fs27 = __toESM(require("fs"));
25463
+ var path36 = __toESM(require("path"));
25268
25464
  var import_crypto = require("crypto");
25269
25465
  function storePath4(dataDir) {
25270
- return path35.join(dataDir, "instance.json");
25466
+ return path36.join(dataDir, "instance.json");
25271
25467
  }
25272
25468
  var InstanceIdentityStore = class {
25273
25469
  constructor(dataDir) {
@@ -25284,7 +25480,7 @@ var InstanceIdentityStore = class {
25284
25480
  const p = storePath4(this.dataDir);
25285
25481
  let raw;
25286
25482
  try {
25287
- raw = fs26.readFileSync(p, "utf8");
25483
+ raw = fs27.readFileSync(p, "utf8");
25288
25484
  } catch (err) {
25289
25485
  if (err.code === "ENOENT") return null;
25290
25486
  throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
@@ -25307,10 +25503,10 @@ var InstanceIdentityStore = class {
25307
25503
  * never truncates the file. Only called by the registry's create-once seed. */
25308
25504
  replace(identity) {
25309
25505
  const p = storePath4(this.dataDir);
25310
- fs26.mkdirSync(path35.dirname(p), { recursive: true });
25506
+ fs27.mkdirSync(path36.dirname(p), { recursive: true });
25311
25507
  const tmp = `${p}.tmp`;
25312
- fs26.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
25313
- fs26.renameSync(tmp, p);
25508
+ fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
25509
+ fs27.renameSync(tmp, p);
25314
25510
  }
25315
25511
  };
25316
25512
  var InstanceIdentityRegistry = class {
@@ -25366,8 +25562,8 @@ function getInstanceIdentity(dataDir) {
25366
25562
  }
25367
25563
 
25368
25564
  // src/utils/secrets.ts
25369
- var fs27 = __toESM(require("fs"));
25370
- var path36 = __toESM(require("path"));
25565
+ var fs28 = __toESM(require("fs"));
25566
+ var path37 = __toESM(require("path"));
25371
25567
  var ENV_FALLBACK = {
25372
25568
  "git-token": ["WAIRON_GIT_TOKEN"],
25373
25569
  "notion-token": ["WAIRON_NOTION_TOKEN"],
@@ -25376,13 +25572,13 @@ var ENV_FALLBACK = {
25376
25572
  };
25377
25573
  function storePath5() {
25378
25574
  const dataDir = process.env["WAIRON_DATA_DIR"];
25379
- return dataDir ? path36.join(dataDir, "auth", "secrets.json") : null;
25575
+ return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
25380
25576
  }
25381
25577
  function readStore() {
25382
25578
  const p = storePath5();
25383
25579
  if (!p) return {};
25384
25580
  try {
25385
- return JSON.parse(fs27.readFileSync(p, "utf8"));
25581
+ return JSON.parse(fs28.readFileSync(p, "utf8"));
25386
25582
  } catch {
25387
25583
  return {};
25388
25584
  }
@@ -25407,10 +25603,10 @@ function setSecret(key, value) {
25407
25603
  if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
25408
25604
  const store = readStore();
25409
25605
  store[key] = value;
25410
- fs27.mkdirSync(path36.dirname(p), { recursive: true });
25606
+ fs28.mkdirSync(path37.dirname(p), { recursive: true });
25411
25607
  const tmp = `${p}.tmp`;
25412
- fs27.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
25413
- fs27.renameSync(tmp, p);
25608
+ fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
25609
+ fs28.renameSync(tmp, p);
25414
25610
  }
25415
25611
  function listSecretKeys() {
25416
25612
  return Object.keys(readStore());
@@ -25618,17 +25814,17 @@ function verifySsoState(state) {
25618
25814
  }
25619
25815
 
25620
25816
  // src/server/organization.ts
25621
- var fs28 = __toESM(require("fs"));
25622
- var path37 = __toESM(require("path"));
25817
+ var fs29 = __toESM(require("fs"));
25818
+ var path38 = __toESM(require("path"));
25623
25819
  var crypto6 = __toESM(require("crypto"));
25624
25820
  function storePath6(dataDir) {
25625
- return path37.join(dataDir, "organization.json");
25821
+ return path38.join(dataDir, "organization.json");
25626
25822
  }
25627
25823
  function readState(dataDir) {
25628
25824
  const p = storePath6(dataDir);
25629
25825
  let raw;
25630
25826
  try {
25631
- raw = fs28.readFileSync(p, "utf8");
25827
+ raw = fs29.readFileSync(p, "utf8");
25632
25828
  } catch (e) {
25633
25829
  if (e.code === "ENOENT") return { units: [], placements: [] };
25634
25830
  throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
@@ -25645,10 +25841,10 @@ function readState(dataDir) {
25645
25841
  }
25646
25842
  function persistState(dataDir, state) {
25647
25843
  const p = storePath6(dataDir);
25648
- fs28.mkdirSync(path37.dirname(p), { recursive: true });
25844
+ fs29.mkdirSync(path38.dirname(p), { recursive: true });
25649
25845
  const tmp = `${p}.tmp`;
25650
- fs28.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25651
- fs28.renameSync(tmp, p);
25846
+ fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25847
+ fs29.renameSync(tmp, p);
25652
25848
  }
25653
25849
  var SLUG_PATTERN = /^[a-z0-9-]+$/;
25654
25850
  var UNIT_KINDS = ["business_entity", "department", "team", "group"];
@@ -26017,11 +26213,11 @@ function getOrganizationUnit(dataDir, id) {
26017
26213
  }
26018
26214
 
26019
26215
  // src/server/permissions.ts
26020
- var fs29 = __toESM(require("fs"));
26021
- var path38 = __toESM(require("path"));
26216
+ var fs30 = __toESM(require("fs"));
26217
+ var path39 = __toESM(require("path"));
26022
26218
  var import_crypto2 = require("crypto");
26023
26219
  function storePath7(dataDir) {
26024
- return path38.join(dataDir, "permissions.json");
26220
+ return path39.join(dataDir, "permissions.json");
26025
26221
  }
26026
26222
  function assignmentKey(a) {
26027
26223
  return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
@@ -26030,7 +26226,7 @@ function load4(dataDir) {
26030
26226
  const p = storePath7(dataDir);
26031
26227
  let raw;
26032
26228
  try {
26033
- raw = fs29.readFileSync(p, "utf8");
26229
+ raw = fs30.readFileSync(p, "utf8");
26034
26230
  } catch (err) {
26035
26231
  if (err.code === "ENOENT") return [];
26036
26232
  throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
@@ -26048,10 +26244,10 @@ function load4(dataDir) {
26048
26244
  }
26049
26245
  function replaceAll2(dataDir, assignments) {
26050
26246
  const p = storePath7(dataDir);
26051
- fs29.mkdirSync(path38.dirname(p), { recursive: true });
26247
+ fs30.mkdirSync(path39.dirname(p), { recursive: true });
26052
26248
  const tmp = `${p}.tmp`;
26053
- fs29.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
26054
- fs29.renameSync(tmp, p);
26249
+ fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
26250
+ fs30.renameSync(tmp, p);
26055
26251
  }
26056
26252
  function registrySet(dataDir, assignment) {
26057
26253
  const assignments = load4(dataDir);
@@ -26137,8 +26333,8 @@ function getAssignment(dataDir, assignmentId) {
26137
26333
  }
26138
26334
 
26139
26335
  // src/server/roles.ts
26140
- var fs30 = __toESM(require("fs"));
26141
- var path39 = __toESM(require("path"));
26336
+ var fs31 = __toESM(require("fs"));
26337
+ var path40 = __toESM(require("path"));
26142
26338
  var BUILTIN_ROLES = [
26143
26339
  {
26144
26340
  id: SSO_ADMIN_ROLE_ID,
@@ -26156,13 +26352,13 @@ function isBuiltinRoleId(roleId) {
26156
26352
  return BUILTIN_ROLE_IDS.has(roleId);
26157
26353
  }
26158
26354
  function storePath8(dataDir) {
26159
- return path39.join(dataDir, "roles.json");
26355
+ return path40.join(dataDir, "roles.json");
26160
26356
  }
26161
26357
  function load5(dataDir) {
26162
26358
  const p = storePath8(dataDir);
26163
26359
  let raw;
26164
26360
  try {
26165
- raw = fs30.readFileSync(p, "utf8");
26361
+ raw = fs31.readFileSync(p, "utf8");
26166
26362
  } catch (err) {
26167
26363
  if (err.code === "ENOENT") return [];
26168
26364
  throw new Error(`Cannot read role store at ${p}: ${err.message}`);
@@ -26180,10 +26376,10 @@ function load5(dataDir) {
26180
26376
  }
26181
26377
  function replaceAll3(dataDir, roles) {
26182
26378
  const p = storePath8(dataDir);
26183
- fs30.mkdirSync(path39.dirname(p), { recursive: true });
26379
+ fs31.mkdirSync(path40.dirname(p), { recursive: true });
26184
26380
  const tmp = `${p}.tmp`;
26185
- fs30.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
26186
- fs30.renameSync(tmp, p);
26381
+ fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
26382
+ fs31.renameSync(tmp, p);
26187
26383
  }
26188
26384
  function registryCreate(dataDir, role) {
26189
26385
  if (isBuiltinRoleId(role.id)) {
@@ -26417,131 +26613,14 @@ function actionableUnitIds(scopes) {
26417
26613
  }
26418
26614
 
26419
26615
  // src/server/projects.ts
26420
- var fs31 = __toESM(require("fs"));
26421
- var path40 = __toESM(require("path"));
26422
- var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
26423
- function isValidProjectId(id) {
26424
- return typeof id === "string" && ID_RE.test(id);
26425
- }
26426
- function registryPath(dataDir) {
26427
- return path40.join(dataDir, "projects.json");
26428
- }
26429
- function load6(dataDir) {
26430
- try {
26431
- return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
26432
- } catch {
26433
- return [];
26434
- }
26435
- }
26436
- function save2(dataDir, records) {
26437
- const p = registryPath(dataDir);
26438
- fs31.mkdirSync(path40.dirname(p), { recursive: true });
26439
- const tmp = `${p}.tmp`;
26440
- fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
26441
- fs31.renameSync(tmp, p);
26442
- }
26443
- function projectRoot(dataDir, id) {
26444
- return path40.join(dataDir, "projects", id);
26445
- }
26446
- function existingProjectRoot(dataDir, id) {
26447
- if (!isValidProjectId(id)) return null;
26448
- const rec = load6(dataDir).find((r) => r.id === id);
26449
- return rec ? rec.rootPath : null;
26450
- }
26451
- function createProjectRecord(dataDir, id) {
26452
- if (!isValidProjectId(id)) {
26453
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
26454
- }
26455
- const records = load6(dataDir);
26456
- if (records.some((r) => r.id === id)) {
26457
- throw new Error(`Project "${id}" already exists.`);
26458
- }
26459
- const root = projectRoot(dataDir, id);
26460
- fs31.mkdirSync(root, { recursive: true });
26461
- const record2 = {
26462
- id,
26463
- rootPath: root,
26464
- status: "active",
26465
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
26466
- };
26467
- records.push(record2);
26468
- save2(dataDir, records);
26469
- return record2;
26470
- }
26471
- function registerLocalDevProject(dataDir, id, rootPath) {
26472
- if (!isValidProjectId(id)) {
26473
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
26474
- }
26475
- const records = load6(dataDir);
26476
- const existing = records.find((r) => r.id === id);
26477
- const record2 = {
26478
- id,
26479
- rootPath,
26480
- status: "active",
26481
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
26482
- };
26483
- const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
26484
- save2(dataDir, next);
26485
- return record2;
26486
- }
26487
- function listProjectRecords(dataDir) {
26488
- return load6(dataDir);
26489
- }
26490
- function removeProjectRecord(dataDir, id) {
26491
- const records = load6(dataDir);
26492
- const rec = records.find((r) => r.id === id);
26493
- if (rec) {
26494
- try {
26495
- fs31.rmSync(rec.rootPath, { recursive: true, force: true });
26496
- } catch {
26497
- }
26498
- }
26499
- save2(dataDir, records.filter((r) => r.id !== id));
26500
- }
26501
- function resolveProjectRoot(dataDir, principal, selector) {
26502
- const authorized = principal.projects;
26503
- const wildcard = authorized.includes("*");
26504
- let target;
26505
- if (selector) {
26506
- if (!wildcard && !authorized.includes(selector)) return null;
26507
- target = selector;
26508
- } else if (!wildcard && authorized.length === 1) {
26509
- target = authorized[0];
26510
- } else {
26511
- return null;
26512
- }
26513
- if (!isValidProjectId(target)) return null;
26514
- const rec = load6(dataDir).find((r) => r.id === target);
26515
- if (!rec || rec.status !== "active") return null;
26516
- return rec.rootPath;
26517
- }
26518
-
26519
- // src/server/adapters.ts
26520
- init_statehash();
26521
-
26522
- // src/core/lockfile.ts
26523
- var fs32 = __toESM(require("fs"));
26524
- var path41 = __toESM(require("path"));
26616
+ var fs35 = __toESM(require("fs"));
26617
+ var path44 = __toESM(require("path"));
26618
+ init_loader();
26619
+ init_yaml();
26525
26620
  init_fs();
26526
- function lockPath() {
26527
- return aiDir("lock.json");
26528
- }
26529
- function readLockRecord() {
26530
- try {
26531
- return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
26532
- } catch {
26533
- return null;
26534
- }
26535
- }
26536
- function writeLockRecord(record2) {
26537
- const p = lockPath();
26538
- fs32.mkdirSync(path41.dirname(p), { recursive: true });
26539
- const tmp = `${p}.tmp`;
26540
- fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
26541
- fs32.renameSync(tmp, p);
26542
- }
26543
26621
 
26544
26622
  // src/server/adapters.ts
26623
+ init_statehash();
26545
26624
  init_specs2();
26546
26625
  init_provision();
26547
26626
  init_validation();
@@ -26552,37 +26631,37 @@ init_types();
26552
26631
  init_server();
26553
26632
 
26554
26633
  // src/git/config.ts
26555
- var fs33 = __toESM(require("fs"));
26556
- var path42 = __toESM(require("path"));
26634
+ var fs32 = __toESM(require("fs"));
26635
+ var path41 = __toESM(require("path"));
26557
26636
  init_fs();
26558
26637
  function configPath() {
26559
26638
  return aiDir("git.json");
26560
26639
  }
26561
26640
  function readGitConfig() {
26562
26641
  try {
26563
- return JSON.parse(fs33.readFileSync(configPath(), "utf8"));
26642
+ return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
26564
26643
  } catch {
26565
26644
  return null;
26566
26645
  }
26567
26646
  }
26568
26647
  function writeGitConfig(config) {
26569
26648
  const p = configPath();
26570
- fs33.mkdirSync(path42.dirname(p), { recursive: true });
26649
+ fs32.mkdirSync(path41.dirname(p), { recursive: true });
26571
26650
  const tmp = `${p}.tmp`;
26572
- fs33.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
26573
- fs33.renameSync(tmp, p);
26651
+ fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
26652
+ fs32.renameSync(tmp, p);
26574
26653
  }
26575
26654
  function clearGitConfig() {
26576
26655
  try {
26577
- fs33.rmSync(configPath(), { force: true });
26656
+ fs32.rmSync(configPath(), { force: true });
26578
26657
  } catch {
26579
26658
  }
26580
26659
  }
26581
26660
 
26582
26661
  // src/git/adapter.ts
26583
26662
  var import_child_process3 = require("child_process");
26584
- var fs34 = __toESM(require("fs"));
26585
- var path43 = __toESM(require("path"));
26663
+ var fs33 = __toESM(require("fs"));
26664
+ var path42 = __toESM(require("path"));
26586
26665
  init_fs();
26587
26666
  function git(args, cwd) {
26588
26667
  return (0, import_child_process3.execFileSync)("git", args, {
@@ -26634,10 +26713,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
26634
26713
  return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
26635
26714
  }
26636
26715
  function excludeLocalFiles() {
26637
- const excludePath = path43.join(getProjectRoot(), ".git", "info", "exclude");
26716
+ const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
26638
26717
  try {
26639
- fs34.mkdirSync(path43.dirname(excludePath), { recursive: true });
26640
- fs34.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
26718
+ fs33.mkdirSync(path42.dirname(excludePath), { recursive: true });
26719
+ fs33.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
26641
26720
  } catch {
26642
26721
  }
26643
26722
  }
@@ -26702,39 +26781,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
26702
26781
  }
26703
26782
 
26704
26783
  // src/producers/config.ts
26705
- var fs35 = __toESM(require("fs"));
26706
- var path44 = __toESM(require("path"));
26784
+ var fs34 = __toESM(require("fs"));
26785
+ var path43 = __toESM(require("path"));
26707
26786
  init_fs();
26708
26787
  function configPath2() {
26709
26788
  return aiDir("producers.json");
26710
26789
  }
26711
- function load7() {
26790
+ function load6() {
26712
26791
  try {
26713
- return JSON.parse(fs35.readFileSync(configPath2(), "utf8"));
26792
+ return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
26714
26793
  } catch {
26715
26794
  return [];
26716
26795
  }
26717
26796
  }
26718
- function save3(configs) {
26797
+ function save2(configs) {
26719
26798
  const p = configPath2();
26720
- fs35.mkdirSync(path44.dirname(p), { recursive: true });
26799
+ fs34.mkdirSync(path43.dirname(p), { recursive: true });
26721
26800
  const tmp = `${p}.tmp`;
26722
- fs35.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26723
- fs35.renameSync(tmp, p);
26801
+ fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26802
+ fs34.renameSync(tmp, p);
26724
26803
  }
26725
26804
  function readProducerConfig(target) {
26726
- return load7().find((c) => c.target === target) ?? null;
26805
+ return load6().find((c) => c.target === target) ?? null;
26727
26806
  }
26728
26807
  function writeProducerConfig(config) {
26729
- const configs = load7().filter((c) => c.target !== config.target);
26808
+ const configs = load6().filter((c) => c.target !== config.target);
26730
26809
  configs.push(config);
26731
- save3(configs);
26810
+ save2(configs);
26732
26811
  }
26733
26812
  function clearProducerConfig(target) {
26734
- save3(load7().filter((c) => c.target !== target));
26813
+ save2(load6().filter((c) => c.target !== target));
26735
26814
  }
26736
26815
  function listProducerConfigs() {
26737
- return load7();
26816
+ return load6();
26738
26817
  }
26739
26818
 
26740
26819
  // src/producers/core-adapter.ts
@@ -27073,8 +27152,19 @@ var hostCore = {
27073
27152
  /** The ids of wairon's built-in architectural profiles, read from the core rules
27074
27153
  * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
27075
27154
  * read of a bundled constant. */
27076
- builtinProfileIds: () => [...BUILTIN_PROFILES]
27155
+ builtinProfileIds: () => [...BUILTIN_PROFILES],
27156
+ /** The ids of wairon's built-in COMPOSITE PROJECT KINDS, read from the core
27157
+ * rules registry's bundled constant (PROJECT_KINDS) — a pure, side-effect-free
27158
+ * read. The counterpart of builtinProfileIds: legal projectType values that
27159
+ * are not architectural profiles and carry no profile doctrine of their own,
27160
+ * so the hosted profile-application path recognizes a project kind as
27161
+ * resolvable-as-is (no contributing pack to adopt) instead of refusing it as
27162
+ * an unknown profile. */
27163
+ builtinProjectKinds: () => [...PROJECT_KINDS]
27077
27164
  };
27165
+ function resolveContainedProjectPath(projectRoot2, projectPath) {
27166
+ return assertContainedProjectPath(projectRoot2, projectPath);
27167
+ }
27078
27168
  function validateProjectAsComplete() {
27079
27169
  const config = loadProjectConfig();
27080
27170
  return validateAsComplete({ rules: config.rules, projectType: config.projectType });
@@ -27106,6 +27196,180 @@ var hostSdk = {
27106
27196
  extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
27107
27197
  };
27108
27198
 
27199
+ // src/server/projects.ts
27200
+ var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
27201
+ function isValidProjectId(id) {
27202
+ return typeof id === "string" && ID_RE.test(id);
27203
+ }
27204
+ function registryPath(dataDir) {
27205
+ return path44.join(dataDir, "projects.json");
27206
+ }
27207
+ function load7(dataDir) {
27208
+ try {
27209
+ return JSON.parse(fs35.readFileSync(registryPath(dataDir), "utf8"));
27210
+ } catch {
27211
+ return [];
27212
+ }
27213
+ }
27214
+ function save3(dataDir, records) {
27215
+ const p = registryPath(dataDir);
27216
+ fs35.mkdirSync(path44.dirname(p), { recursive: true });
27217
+ const tmp = `${p}.tmp`;
27218
+ fs35.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
27219
+ fs35.renameSync(tmp, p);
27220
+ }
27221
+ function projectRoot(dataDir, id) {
27222
+ return path44.join(dataDir, "projects", id);
27223
+ }
27224
+ function existingProjectRoot(dataDir, id) {
27225
+ if (!isValidProjectId(id)) return null;
27226
+ const rec = load7(dataDir).find((r) => r.id === id);
27227
+ return rec ? rec.rootPath : null;
27228
+ }
27229
+ function createProjectRecord(dataDir, id) {
27230
+ if (!isValidProjectId(id)) {
27231
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27232
+ }
27233
+ const records = load7(dataDir);
27234
+ if (records.some((r) => r.id === id)) {
27235
+ throw new Error(`Project "${id}" already exists.`);
27236
+ }
27237
+ const root = projectRoot(dataDir, id);
27238
+ fs35.mkdirSync(root, { recursive: true });
27239
+ const record2 = {
27240
+ id,
27241
+ rootPath: root,
27242
+ status: "active",
27243
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
27244
+ };
27245
+ records.push(record2);
27246
+ save3(dataDir, records);
27247
+ return record2;
27248
+ }
27249
+ function registerLocalDevProject(dataDir, id, rootPath) {
27250
+ if (!isValidProjectId(id)) {
27251
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27252
+ }
27253
+ const records = load7(dataDir);
27254
+ const existing = records.find((r) => r.id === id);
27255
+ const record2 = {
27256
+ id,
27257
+ rootPath,
27258
+ status: "active",
27259
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
27260
+ };
27261
+ const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
27262
+ save3(dataDir, next);
27263
+ return record2;
27264
+ }
27265
+ function listProjectRecords(dataDir) {
27266
+ return load7(dataDir);
27267
+ }
27268
+ function removeProjectRecord(dataDir, id) {
27269
+ const records = load7(dataDir);
27270
+ const rec = records.find((r) => r.id === id);
27271
+ if (rec) {
27272
+ try {
27273
+ fs35.rmSync(rec.rootPath, { recursive: true, force: true });
27274
+ } catch {
27275
+ }
27276
+ }
27277
+ save3(dataDir, records.filter((r) => r.id !== id));
27278
+ }
27279
+ var SUBPROJECT_SEPARATOR = "::";
27280
+ function parseQualifiedSelector(value) {
27281
+ if (typeof value !== "string" || value.length === 0) return null;
27282
+ const [projectId, ...mounts] = value.split(SUBPROJECT_SEPARATOR);
27283
+ if (!isValidProjectId(projectId)) return null;
27284
+ if (mounts.some((m) => m.trim() === "")) return null;
27285
+ return { projectId, mounts };
27286
+ }
27287
+ function findSubsystemSpec(root, subsystemId) {
27288
+ const specsDir = aiPathsAt(root).specsDir();
27289
+ if (!fs35.existsSync(specsDir)) return null;
27290
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
27291
+ let raw;
27292
+ try {
27293
+ raw = readYamlFile(file);
27294
+ } catch {
27295
+ continue;
27296
+ }
27297
+ if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
27298
+ if (raw.id !== subsystemId) continue;
27299
+ const pp = raw.projectPath;
27300
+ return typeof pp === "string" && pp.trim() !== "" ? { projectPath: pp } : {};
27301
+ }
27302
+ return null;
27303
+ }
27304
+ function resolveSubprojectMounts(projectId, projectRoot2, mounts) {
27305
+ let root = projectRoot2;
27306
+ let at = projectId;
27307
+ for (const mount of mounts) {
27308
+ const sub = findSubsystemSpec(root, mount);
27309
+ if (!sub) {
27310
+ throw new Error(
27311
+ `unknown subproject mount "${mount}" on "${at}" \u2014 no subsystem with that id exists in its spec tree`
27312
+ );
27313
+ }
27314
+ if (!sub.projectPath) {
27315
+ throw new Error(
27316
+ `subsystem "${mount}" on "${at}" is not a chained subproject (it carries no projectPath) \u2014 only a subsystem mounted via projectPath can be bound as a subproject`
27317
+ );
27318
+ }
27319
+ root = resolveContainedProjectPath(root, sub.projectPath);
27320
+ at = `${at}${SUBPROJECT_SEPARATOR}${mount}`;
27321
+ }
27322
+ return root;
27323
+ }
27324
+ function assertMintableNarrowingEntry(dataDir, entry) {
27325
+ if (entry === "*") return;
27326
+ const parsed = parseQualifiedSelector(entry);
27327
+ if (!parsed) {
27328
+ throw new Error(
27329
+ `invalid project narrowing entry "${entry}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
27330
+ );
27331
+ }
27332
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27333
+ if (!rec) throw new Error(`unknown project "${parsed.projectId}"`);
27334
+ if (parsed.mounts.length > 0) {
27335
+ resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27336
+ }
27337
+ }
27338
+ function narrowingCovers(entry, target) {
27339
+ return target === entry || target.startsWith(entry + SUBPROJECT_SEPARATOR);
27340
+ }
27341
+ function resolveProjectBinding(dataDir, principal, selector) {
27342
+ const authorized = principal.projects;
27343
+ const wildcard = authorized.includes("*");
27344
+ let target;
27345
+ if (selector) {
27346
+ if (!wildcard && !authorized.some((e) => e !== "*" && narrowingCovers(e, selector))) return null;
27347
+ target = selector;
27348
+ } else if (!wildcard && authorized.length === 1) {
27349
+ target = authorized[0];
27350
+ } else {
27351
+ return null;
27352
+ }
27353
+ const parsed = parseQualifiedSelector(target);
27354
+ if (!parsed) return null;
27355
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27356
+ if (!rec || rec.status !== "active") return null;
27357
+ let rootPath = rec.rootPath;
27358
+ if (parsed.mounts.length > 0) {
27359
+ try {
27360
+ rootPath = resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27361
+ } catch {
27362
+ return null;
27363
+ }
27364
+ }
27365
+ const binding = { rootPath, projectId: parsed.projectId };
27366
+ if (parsed.mounts.length > 0) binding.subproject = parsed.mounts.join(SUBPROJECT_SEPARATOR);
27367
+ return binding;
27368
+ }
27369
+ function resolveProjectRoot(dataDir, principal, selector) {
27370
+ return resolveProjectBinding(dataDir, principal, selector)?.rootPath ?? null;
27371
+ }
27372
+
27109
27373
  // src/server/errors.ts
27110
27374
  var UnauthenticatedError = class extends Error {
27111
27375
  constructor() {
@@ -27517,6 +27781,35 @@ function storeListGlobalPacks() {
27517
27781
  );
27518
27782
  return [...instance, ...image];
27519
27783
  }
27784
+ function scanGlobalPackProfiles() {
27785
+ const out = [];
27786
+ for (const dir of [hostCore.globalPacksDir(), imagePacksDir()]) {
27787
+ for (const full of hostCore.discoverPacks(dir)) {
27788
+ try {
27789
+ const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
27790
+ if (loaded.errors.length) continue;
27791
+ const source = loaded.packNames[0] ?? path45.basename(full);
27792
+ for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
27793
+ } catch {
27794
+ }
27795
+ }
27796
+ }
27797
+ return out;
27798
+ }
27799
+ function scanProjectPackProfiles() {
27800
+ const root = getProjectRoot();
27801
+ const out = [];
27802
+ for (const ref of loadProjectConfig().extensions?.packs ?? []) {
27803
+ try {
27804
+ const loaded = hostCore.loadExtensionPacks([{ ref, scope: "project" }], root);
27805
+ if (loaded.errors.length) continue;
27806
+ const source = loaded.packNames[0] ?? stem(ref);
27807
+ for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
27808
+ } catch {
27809
+ }
27810
+ }
27811
+ return out;
27812
+ }
27520
27813
  function storeListAvailableProfiles() {
27521
27814
  const out = [];
27522
27815
  const seen = /* @__PURE__ */ new Set();
@@ -27527,19 +27820,21 @@ function storeListAvailableProfiles() {
27527
27820
  out.push(family ? { id, source, family } : { id, source });
27528
27821
  };
27529
27822
  for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
27530
- const scanTier = (dir) => {
27531
- for (const full of hostCore.discoverPacks(dir)) {
27532
- try {
27533
- const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
27534
- if (loaded.errors.length) continue;
27535
- const source = loaded.packNames[0] ?? path45.basename(full);
27536
- for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
27537
- } catch {
27538
- }
27539
- }
27823
+ for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, c.family);
27824
+ return out;
27825
+ }
27826
+ function storeListProjectProfiles() {
27827
+ const out = [];
27828
+ const seen = /* @__PURE__ */ new Set();
27829
+ const emit = (id, source, installed, family) => {
27830
+ const key = JSON.stringify([id, source]);
27831
+ if (seen.has(key)) return;
27832
+ seen.add(key);
27833
+ out.push({ id, source, ...family ? { family } : {}, installed });
27540
27834
  };
27541
- scanTier(hostCore.globalPacksDir());
27542
- scanTier(imagePacksDir());
27835
+ for (const id of hostCore.builtinProfileIds()) emit(id, "builtin", true);
27836
+ for (const c of scanProjectPackProfiles()) emit(c.id, c.source, true, c.family);
27837
+ for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, false, c.family);
27543
27838
  return out;
27544
27839
  }
27545
27840
  function readPackContent(full) {
@@ -27732,6 +28027,10 @@ function listAvailableProfiles(cfg, credential) {
27732
28027
  requirePrincipal2(cfg, credential);
27733
28028
  return storeListAvailableProfiles();
27734
28029
  }
28030
+ function listProjectProfiles(cfg, credential, project2) {
28031
+ requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing a project's selectable profiles requires project:read over the project");
28032
+ return executeApprovedListProjectProfiles(cfg, project2);
28033
+ }
27735
28034
  function listAdoptableProjectPacks(cfg, credential, project2) {
27736
28035
  requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
27737
28036
  return storeListGlobalPacks();
@@ -27754,6 +28053,26 @@ function executeApprovedInstallProjectPack(cfg, project2, name, content) {
27754
28053
  function executeApprovedResolveGlobalPacks(names) {
27755
28054
  return storeResolveGlobalPacks(names);
27756
28055
  }
28056
+ function executeApprovedListProjectProfiles(cfg, project2) {
28057
+ return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectProfiles());
28058
+ }
28059
+ function executeApprovedEnsureProfileInstalled(cfg, project2, profileId) {
28060
+ if (hostCore.builtinProjectKinds().includes(profileId) || hostCore.builtinProfileIds().includes(profileId)) {
28061
+ return { profileId, source: "builtin" };
28062
+ }
28063
+ const contributors = executeApprovedListProjectProfiles(cfg, project2).filter((p) => p.id === profileId);
28064
+ const installed = contributors.find((p) => p.installed);
28065
+ if (installed) return { profileId, source: installed.source };
28066
+ const adoptable = contributors[0];
28067
+ const resolved = adoptable ? executeApprovedResolveGlobalPacks([adoptable.source]).resolved[0] : void 0;
28068
+ if (!resolved) {
28069
+ throw new Error(
28070
+ `Unknown profile "${profileId}" \u2014 no built-in profile or project kind carries it, no pack registered in project "${project2}" contributes it, and no server-global pack (mutable instance tier or immutable image tier) contributes it. Writing an unresolvable id as the projectType would silently disable the whole profile doctrine (UNKNOWN_PROFILE), so it is refused instead of applied.`
28071
+ );
28072
+ }
28073
+ executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
28074
+ return { profileId, source: resolved.name, adoptedPackName: resolved.name };
28075
+ }
27757
28076
  function removeProjectPack(cfg, credential, project2, name) {
27758
28077
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
27759
28078
  runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
@@ -28748,6 +29067,40 @@ function requestPackNames(request) {
28748
29067
  if (!sel) return [];
28749
29068
  return [.../* @__PURE__ */ new Set([...sel.requiredPackNames ?? [], ...sel.defaultPackNames ?? []])];
28750
29069
  }
29070
+ function classifyProfile(projectType, catalog) {
29071
+ if (hostCore.builtinProfileIds().includes(projectType) || hostCore.builtinProjectKinds().includes(projectType)) {
29072
+ return { source: "builtin", resolvable: true };
29073
+ }
29074
+ const installed = catalog.find((p) => p.id === projectType && p.installed);
29075
+ if (installed) return { source: installed.source, resolvable: true };
29076
+ return { resolvable: false };
29077
+ }
29078
+ function firstApplicableProfileId(candidates, catalog) {
29079
+ const kinds = hostCore.builtinProjectKinds();
29080
+ const catalogIds = new Set(catalog.map((p) => p.id));
29081
+ return candidates.find((id) => kinds.includes(id) || catalogIds.has(id));
29082
+ }
29083
+ function unappliedIds(selectedProfileIds, governingProfileId) {
29084
+ return selectedProfileIds.filter((id) => id !== governingProfileId);
29085
+ }
29086
+ function foldAppliedProfile(root, profileId, actor) {
29087
+ const recorded = readProjectProfileSelection(root);
29088
+ const folded = {
29089
+ profileIds: [profileId, ...unappliedIds(recorded?.profileIds ?? [], profileId)],
29090
+ requiredPackNames: recorded?.requiredPackNames ?? [],
29091
+ selectedBy: actor,
29092
+ selectedAt: (/* @__PURE__ */ new Date()).toISOString()
29093
+ };
29094
+ if (recorded?.defaultPackNames) folded.defaultPackNames = recorded.defaultPackNames;
29095
+ recordProjectProfileSelection(root, folded);
29096
+ return folded;
29097
+ }
29098
+ function overridingSubsystemIds(root) {
29099
+ return runWithProjectRoot(
29100
+ root,
29101
+ () => hostCore.loadSubsystemSpecs().filter((s) => !!s.profile).map((s) => s.id)
29102
+ );
29103
+ }
28751
29104
  function resolvedSelection(request, policy, selectedBy) {
28752
29105
  const sel = request.profileSelection;
28753
29106
  const selection = {
@@ -28766,7 +29119,8 @@ function buildEvaluation(input) {
28766
29119
  selectedProfileIds,
28767
29120
  hasSelection,
28768
29121
  countMissingPacksAsViolation,
28769
- requiredDefaultResolution
29122
+ requiredDefaultResolution,
29123
+ governingProfileId
28770
29124
  } = input;
28771
29125
  const present = new Set(presentPackNames);
28772
29126
  let missingPackNames;
@@ -28787,6 +29141,7 @@ function buildEvaluation(input) {
28787
29141
  const allowed = policy.allowedProfileIds;
28788
29142
  const disallowedProfileIds = allowed && allowed.length > 0 ? selectedProfileIds.filter((id) => !allowed.includes(id)) : [];
28789
29143
  const selectionRequiredUnmet = policy.requireProfileSelection && !hasSelection;
29144
+ const unappliedProfileIds = governingProfileId ? unappliedIds(selectedProfileIds, governingProfileId) : [];
28790
29145
  const messages = [];
28791
29146
  if (selectionRequiredUnmet) {
28792
29147
  messages.push("Profile selection is required by policy but none was provided.");
@@ -28798,16 +29153,24 @@ function buildEvaluation(input) {
28798
29153
  for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
28799
29154
  for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
28800
29155
  for (const id of disallowedProfileIds) messages.push(`Profile "${id}" is not permitted by policy.`);
29156
+ for (const id of unappliedProfileIds) {
29157
+ messages.push(
29158
+ `Selected profile "${id}" is recorded but does not govern the project (projectType is "${governingProfileId}") \u2014 a project has exactly one governing profile.`
29159
+ );
29160
+ }
28801
29161
  const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
28802
- return {
29162
+ const result = {
28803
29163
  compliant: !violation,
28804
29164
  mode: policy.enforcementMode,
28805
29165
  missingPackNames,
28806
29166
  blockedPackNames,
28807
29167
  missingProfileIds,
28808
29168
  unresolvedPacks,
29169
+ unappliedProfileIds,
28809
29170
  messages
28810
29171
  };
29172
+ if (governingProfileId) result.governingProfileId = governingProfileId;
29173
+ return result;
28811
29174
  }
28812
29175
  function performInit(cfg, request, principal) {
28813
29176
  const policy = effectivePolicy(cfg.dataDir);
@@ -28827,17 +29190,24 @@ function performInit(cfg, request, principal) {
28827
29190
  request.ownerUnitId,
28828
29191
  principal ? principalSubject3(principal) : void 0
28829
29192
  );
28830
- installResolvedPacks(
28831
- cfg,
28832
- record2.id,
28833
- executeApprovedResolveGlobalPacks([
28834
- ...policy.requiredGlobalPacks,
28835
- ...policy.defaultProjectPacks,
28836
- ...requestPackNames(request)
28837
- ])
28838
- );
29193
+ const packResolution = executeApprovedResolveGlobalPacks([
29194
+ ...policy.requiredGlobalPacks,
29195
+ ...policy.defaultProjectPacks,
29196
+ ...requestPackNames(request)
29197
+ ]);
29198
+ installResolvedPacks(cfg, record2.id, packResolution);
28839
29199
  const selectedBy = principal ? principalSubject3(principal) : void 0;
28840
- recordProjectProfileSelection(record2.rootPath, resolvedSelection(request, policy, selectedBy));
29200
+ const selection = resolvedSelection(request, policy, selectedBy);
29201
+ recordProjectProfileSelection(record2.rootPath, selection);
29202
+ const catalog = executeApprovedListProjectProfiles(cfg, record2.id);
29203
+ const appliedProfileId = firstApplicableProfileId(selection.profileIds, catalog);
29204
+ let appliedSource;
29205
+ if (appliedProfileId) {
29206
+ const application = executeApprovedEnsureProfileInstalled(cfg, record2.id, appliedProfileId);
29207
+ writeProjectType(record2.rootPath, application.profileId);
29208
+ appliedSource = application.source;
29209
+ }
29210
+ const unapplied = appliedProfileId ? unappliedIds(selection.profileIds, appliedProfileId) : [...selection.profileIds];
28841
29211
  const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
28842
29212
  tryAppendAudit2(
28843
29213
  cfg,
@@ -28846,7 +29216,18 @@ function performInit(cfg, request, principal) {
28846
29216
  "project.init.policy",
28847
29217
  "info",
28848
29218
  "project",
28849
- { target: record2.id, projectId: record2.id },
29219
+ {
29220
+ target: record2.id,
29221
+ projectId: record2.id,
29222
+ metadata: JSON.stringify({
29223
+ ...appliedProfileId ? { appliedProfileId, profileSource: appliedSource } : {
29224
+ appliedProfileId: null,
29225
+ profileNotApplied: selection.profileIds.length === 0 ? "no profile was selected \u2014 the default projectType stands" : "no selected profile is resolvable on this instance \u2014 the default projectType stands"
29226
+ },
29227
+ ...unapplied.length > 0 ? { unappliedProfileIds: unapplied } : {},
29228
+ ...packResolution.unresolved.length > 0 ? { unresolvedPacks: packResolution.unresolved } : {}
29229
+ })
29230
+ },
28850
29231
  principal?.tokenId
28851
29232
  )
28852
29233
  );
@@ -28887,6 +29268,7 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28887
29268
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28888
29269
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28889
29270
  const policy = effectivePolicy(cfg.dataDir);
29271
+ const governingProfileId = readProjectType(root);
28890
29272
  const selection = readProjectProfileSelection(root);
28891
29273
  return buildEvaluation({
28892
29274
  policy,
@@ -28894,7 +29276,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28894
29276
  selectedProfileIds: selection?.profileIds ?? [],
28895
29277
  hasSelection: !!selection,
28896
29278
  countMissingPacksAsViolation: true,
28897
- requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
29279
+ requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy)),
29280
+ governingProfileId
28898
29281
  });
28899
29282
  }
28900
29283
  function reconcileProjectPolicy(cfg, credential, projectId) {
@@ -28909,20 +29292,41 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28909
29292
  const policy = effectivePolicy(cfg.dataDir);
28910
29293
  const selection = readProjectProfileSelection(root);
28911
29294
  const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
28912
- let installed = installedPackNames(cfg, projectId);
28913
- const installedSet = new Set(installed);
29295
+ const installedSet = new Set(installedPackNames(cfg, projectId));
28914
29296
  const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
29297
+ const appliedPackNames = toApply.map((p) => p.name);
28915
29298
  if (toApply.length > 0) {
28916
29299
  installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
28917
- installed = installedPackNames(cfg, projectId);
29300
+ }
29301
+ const catalog = executeApprovedListProjectProfiles(cfg, projectId);
29302
+ const previousProfileId = readProjectType(root);
29303
+ let governingProfileId = previousProfileId;
29304
+ const requiredProfileIds = policy.requiredProfileIds ?? [];
29305
+ const policyUnsatisfied = requiredProfileIds.length > 0 && !requiredProfileIds.includes(governingProfileId);
29306
+ const governingUnresolvable = !classifyProfile(governingProfileId, catalog).resolvable;
29307
+ let repairedProfileId;
29308
+ let selectedProfileIds = selection?.profileIds ?? [];
29309
+ if (policyUnsatisfied || governingUnresolvable) {
29310
+ const target = firstApplicableProfileId(
29311
+ [...requiredProfileIds, ...selection?.profileIds ?? []],
29312
+ catalog
29313
+ );
29314
+ if (target) {
29315
+ const application = executeApprovedEnsureProfileInstalled(cfg, projectId, target);
29316
+ writeProjectType(root, application.profileId);
29317
+ governingProfileId = application.profileId;
29318
+ repairedProfileId = application.profileId;
29319
+ selectedProfileIds = foldAppliedProfile(root, application.profileId, principalSubject3(principal)).profileIds;
29320
+ }
28918
29321
  }
28919
29322
  const result = buildEvaluation({
28920
29323
  policy,
28921
- presentPackNames: installed,
28922
- selectedProfileIds: selection?.profileIds ?? [],
29324
+ presentPackNames: installedPackNames(cfg, projectId),
29325
+ selectedProfileIds,
28923
29326
  hasSelection: !!selection,
28924
29327
  countMissingPacksAsViolation: true,
28925
- requiredDefaultResolution: resolution
29328
+ requiredDefaultResolution: resolution,
29329
+ governingProfileId
28926
29330
  });
28927
29331
  tryAppendAudit2(
28928
29332
  cfg,
@@ -28931,7 +29335,15 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28931
29335
  "policy.reconcile",
28932
29336
  "info",
28933
29337
  "policy",
28934
- { target: projectId, projectId },
29338
+ {
29339
+ target: projectId,
29340
+ projectId,
29341
+ metadata: JSON.stringify({
29342
+ ...appliedPackNames.length > 0 ? { appliedPackNames } : {},
29343
+ ...repairedProfileId ? { repairedProfileId, previousProfileId } : {},
29344
+ ...resolution.unresolved.length > 0 ? { unresolvedPacks: resolution.unresolved } : {}
29345
+ })
29346
+ },
28935
29347
  principal.tokenId
28936
29348
  )
28937
29349
  );
@@ -28947,8 +29359,19 @@ function getProjectConfig(cfg, credential, projectId) {
28947
29359
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28948
29360
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28949
29361
  const projectType = readProjectType(root);
29362
+ const selection = readProjectProfileSelection(root);
29363
+ const classified = classifyProfile(projectType, executeApprovedListProjectProfiles(cfg, projectId));
28950
29364
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28951
- return { projectType, locked };
29365
+ const overriding = overridingSubsystemIds(root);
29366
+ const view = {
29367
+ projectType,
29368
+ locked,
29369
+ profileResolvable: classified.resolvable,
29370
+ unappliedProfileIds: unappliedIds(selection?.profileIds ?? [], projectType),
29371
+ overridingSubsystemIds: overriding
29372
+ };
29373
+ if (classified.source) view.profileSource = classified.source;
29374
+ return view;
28952
29375
  }
28953
29376
  function setProjectType(cfg, credential, projectId, projectType) {
28954
29377
  const principal = requirePrincipal4(cfg, credential);
@@ -28959,9 +29382,23 @@ function setProjectType(cfg, credential, projectId, projectType) {
28959
29382
  }
28960
29383
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28961
29384
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28962
- writeProjectType(root, projectType);
29385
+ const application = executeApprovedEnsureProfileInstalled(cfg, projectId, projectType);
29386
+ writeProjectType(root, application.profileId);
29387
+ const folded = foldAppliedProfile(root, application.profileId, principalSubject3(principal));
29388
+ const remainder = folded.profileIds.slice(1);
28963
29389
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28964
- return { projectType, locked };
29390
+ const overriding = overridingSubsystemIds(root);
29391
+ const view = {
29392
+ projectType: application.profileId,
29393
+ locked,
29394
+ profileSource: application.source,
29395
+ // The write path guarantees resolvability — the ensure seam refused anything else.
29396
+ profileResolvable: true,
29397
+ unappliedProfileIds: remainder,
29398
+ overridingSubsystemIds: overriding
29399
+ };
29400
+ if (application.adoptedPackName) view.adoptedPackName = application.adoptedPackName;
29401
+ return view;
28965
29402
  }
28966
29403
  function getPackPolicy(cfg, credential) {
28967
29404
  requirePrincipal4(cfg, credential);
@@ -29131,11 +29568,8 @@ function mintToken(cfg, credential, request) {
29131
29568
  }
29132
29569
  assertNotReservedSubjectId(cfg, [request.ownerUserId]);
29133
29570
  const projects = request.projects?.length ? request.projects : ["*"];
29134
- const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
29135
29571
  for (const p of projects) {
29136
- if (p !== "*" && !knownProjects.has(p)) {
29137
- throw new Error(`unknown project "${p}"`);
29138
- }
29572
+ assertMintableNarrowingEntry(cfg.dataDir, p);
29139
29573
  }
29140
29574
  const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
29141
29575
  if (owner && owner.status !== "active") {
@@ -29172,12 +29606,21 @@ function revokeToken(cfg, credential, tokenId) {
29172
29606
  }
29173
29607
  function mintSelfToken(cfg, credential, projectId, write) {
29174
29608
  const principal = requirePrincipal5(cfg, credential);
29175
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
29609
+ const parsed = parseQualifiedSelector(projectId);
29610
+ if (!parsed) {
29611
+ throw new Error(
29612
+ `invalid project id "${projectId}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
29613
+ );
29614
+ }
29615
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
29176
29616
  throw new ForbiddenError("caller lacks project:read on the requested project");
29177
29617
  }
29178
- if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
29618
+ if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
29179
29619
  throw new ForbiddenError("caller lacks project:write on the requested project");
29180
29620
  }
29621
+ if (parsed.mounts.length > 0) {
29622
+ assertMintableNarrowingEntry(cfg.dataDir, projectId);
29623
+ }
29181
29624
  const token = "wk_" + crypto10.randomBytes(24).toString("hex");
29182
29625
  const owner = auditActor(principal);
29183
29626
  const record2 = {
@@ -29789,12 +30232,12 @@ function resolveVisibility(observerProjectId, units, placements) {
29789
30232
  const best = /* @__PURE__ */ new Map();
29790
30233
  for (const placement of placements) {
29791
30234
  if (placement.projectId === observerProjectId) continue;
29792
- const path62 = chainOf(placement.unitId, unitById);
29793
- if (!path62.length) continue;
29794
- const closedOk = path62.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
30235
+ const path61 = chainOf(placement.unitId, unitById);
30236
+ if (!path61.length) continue;
30237
+ const closedOk = path61.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
29795
30238
  if (!closedOk) continue;
29796
- const crossTenant = !tenantRoots.has(path62[path62.length - 1].id);
29797
- if (crossTenant && !path62.some(grantedTo)) continue;
30239
+ const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
30240
+ if (crossTenant && !path61.some(grantedTo)) continue;
29798
30241
  if (crossTenant && directUnits.length === 0) continue;
29799
30242
  const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
29800
30243
  const existing = best.get(placement.projectId);
@@ -30733,10 +31176,9 @@ function migratePermissionModel(dataDir, apply) {
30733
31176
  // src/server/http.ts
30734
31177
  var http2 = __toESM(require("http"));
30735
31178
  var fs49 = __toESM(require("fs"));
30736
- var path59 = __toESM(require("path"));
31179
+ var path58 = __toESM(require("path"));
30737
31180
 
30738
31181
  // src/server/request.ts
30739
- var path58 = __toESM(require("path"));
30740
31182
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
30741
31183
  init_fs();
30742
31184
 
@@ -31950,6 +32392,9 @@ function getProjectConfig2(cfg, credential, projectId) {
31950
32392
  function setProjectType2(cfg, credential, projectId, projectType) {
31951
32393
  return setProjectType(cfg, credential, projectId, projectType);
31952
32394
  }
32395
+ function listProjectProfiles2(cfg, credential, project2) {
32396
+ return listProjectProfiles(cfg, credential, project2);
32397
+ }
31953
32398
  function listProducers2(cfg, credential, project2) {
31954
32399
  return listProducers(cfg, credential, project2);
31955
32400
  }
@@ -35311,6 +35756,9 @@ function opsGetProjectConfig(cfg, sessionId, url, res) {
35311
35756
  function opsSetProjectConfig(cfg, sessionId, body, res) {
35312
35757
  sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
35313
35758
  }
35759
+ function opsListProjectProfiles(cfg, sessionId, url, res) {
35760
+ sendJson(res, 200, { profiles: listProjectProfiles2(cfg, sessionId, q(url, "projectId") ?? "") });
35761
+ }
35314
35762
  function opsListProducers(cfg, sessionId, url, res) {
35315
35763
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
35316
35764
  }
@@ -35550,6 +35998,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
35550
35998
  if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
35551
35999
  return opsSetProjectConfig(cfg, sessionId, body, res);
35552
36000
  }
36001
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
36002
+ return opsListProjectProfiles(cfg, sessionId, url, res);
36003
+ }
35553
36004
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
35554
36005
  return opsPolicyEvaluate(cfg, sessionId, url, res);
35555
36006
  }
@@ -35774,8 +36225,8 @@ var RealtimeHub = class {
35774
36225
  * complete the handshake, and register the connection. A bad path or session
35775
36226
  * destroys the socket. */
35776
36227
  handleUpgrade(cfg, req, socket) {
35777
- const path62 = (req.url ?? "/").split("?")[0];
35778
- if (path62 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
36228
+ const path61 = (req.url ?? "/").split("?")[0];
36229
+ if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
35779
36230
  socket.destroy();
35780
36231
  return;
35781
36232
  }
@@ -35920,7 +36371,7 @@ function deriveMcpOutcome(response) {
35920
36371
  if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
35921
36372
  return "success";
35922
36373
  }
35923
- function auditToolCall(dataDir, principal, projectId, body, outcome) {
36374
+ function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
35924
36375
  const target = mcpToolTarget(body);
35925
36376
  if (!target) return;
35926
36377
  const actor = principal.subject ?? {
@@ -35938,7 +36389,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
35938
36389
  actor,
35939
36390
  tokenId: principal.tokenId,
35940
36391
  projectId,
35941
- target
36392
+ target,
36393
+ ...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
35942
36394
  };
35943
36395
  try {
35944
36396
  appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
@@ -36142,8 +36594,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
36142
36594
  permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
36143
36595
  };
36144
36596
  }
36145
- const root = resolveProjectRoot(cfg.dataDir, principal, projectSelector(req));
36146
- if (!root) {
36597
+ const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
36598
+ if (!binding) {
36147
36599
  sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
36148
36600
  return;
36149
36601
  }
@@ -36158,19 +36610,20 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
36158
36610
  });
36159
36611
  return;
36160
36612
  }
36161
- await runWithProjectRoot(root, async () => {
36162
- const projectId = path58.basename(root);
36613
+ await runWithProjectRoot(binding.rootPath, async () => {
36614
+ const projectId = binding.projectId;
36615
+ const subproject = binding.subproject;
36163
36616
  const dispatchedResponse = await dispatchProjectLifecycleTool(cfg, cred, projectId, body);
36164
36617
  if (dispatchedResponse !== void 0) {
36165
36618
  sendJson(res, 200, dispatchedResponse);
36166
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
36619
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
36167
36620
  for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
36168
36621
  return;
36169
36622
  }
36170
36623
  const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
36171
36624
  if (permissionError !== void 0) {
36172
36625
  sendJson(res, 200, permissionError);
36173
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
36626
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
36174
36627
  return;
36175
36628
  }
36176
36629
  const server = createScopedServer();
@@ -36191,7 +36644,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
36191
36644
  };
36192
36645
  await server.connect(transport);
36193
36646
  await transport.handleRequest(req, res, body);
36194
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
36647
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
36195
36648
  for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
36196
36649
  });
36197
36650
  }
@@ -36626,7 +37079,7 @@ function routeData(cfg, req, res) {
36626
37079
  }
36627
37080
  function readExposurePolicyFile(dataDir) {
36628
37081
  try {
36629
- const raw = fs49.readFileSync(path59.join(dataDir, "exposure-policy.json"), "utf8");
37082
+ const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
36630
37083
  const parsed = JSON.parse(raw);
36631
37084
  return parsed && typeof parsed === "object" ? parsed : void 0;
36632
37085
  } catch {
@@ -37311,9 +37764,9 @@ function seedDemoTree() {
37311
37764
 
37312
37765
  // src/commands/host.ts
37313
37766
  function resolveHostConfig(options) {
37314
- const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path60.join(os9.homedir(), ".wairon", "data");
37767
+ const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path59.join(os10.homedir(), ".wairon", "data");
37315
37768
  if (!process.env["WAIRON_PACKS_DIR"]) {
37316
- process.env["WAIRON_PACKS_DIR"] = path60.join(dataDir, "packs");
37769
+ process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
37317
37770
  }
37318
37771
  const cfg = {
37319
37772
  host: options.host || "0.0.0.0",
@@ -37438,13 +37891,13 @@ function openBrowser(url) {
37438
37891
  }
37439
37892
  async function runDev(options = {}) {
37440
37893
  const cwd = process.cwd();
37441
- if (!fs50.existsSync(path60.join(cwd, ".wai"))) {
37894
+ if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
37442
37895
  throw new WaironError(
37443
37896
  "No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
37444
37897
  );
37445
37898
  }
37446
37899
  const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
37447
- const dataDir = path60.join(os9.tmpdir(), "wairon-dev", hash);
37900
+ const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
37448
37901
  fs50.mkdirSync(dataDir, { recursive: true });
37449
37902
  registerLocalDevProject(dataDir, "local", cwd);
37450
37903
  const port = options.port ? Number(options.port) : 8080;
@@ -37853,8 +38306,8 @@ async function runHostPacks(action, options = {}) {
37853
38306
  }
37854
38307
  case "install": {
37855
38308
  if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
37856
- const name = options.name ?? path60.basename(options.file).replace(/\.(ya?ml)$/i, "");
37857
- const content = fs50.readFileSync(path60.resolve(options.file), "utf8");
38309
+ const name = options.name ?? path59.basename(options.file).replace(/\.(ya?ml)$/i, "");
38310
+ const content = fs50.readFileSync(path59.resolve(options.file), "utf8");
37858
38311
  const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
37859
38312
  logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
37860
38313
  if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
@@ -38060,13 +38513,27 @@ async function runSurface(action, options = {}) {
38060
38513
  for (const p of written) logger.info(` ${p}`);
38061
38514
  return;
38062
38515
  }
38516
+ case "externals": {
38517
+ const entries = listExternalInterfaces();
38518
+ if (!entries.length) {
38519
+ logger.info("No external surfaces available (.wai/surfaces/ holds no snapshots).");
38520
+ return;
38521
+ }
38522
+ const freshness = (f) => f === "fresh" ? import_chalk19.default.green(f) : f === "stale" ? import_chalk19.default.yellow(f) : import_chalk19.default.gray(f);
38523
+ for (const e of entries) {
38524
+ logger.info(
38525
+ `${e.sourceKind.padEnd(8)} ${import_chalk19.default.cyan(e.projectName)} [${e.origin}] ${freshness(e.freshness)} \u2014 ${e.interfaceIds.length ? e.interfaceIds.join(", ") : "(no interfaces)"}`
38526
+ );
38527
+ }
38528
+ return;
38529
+ }
38063
38530
  default:
38064
- throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children).`);
38531
+ throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children, externals).`);
38065
38532
  }
38066
38533
  }
38067
38534
 
38068
38535
  // src/commands/subsystem.ts
38069
- var path61 = __toESM(require("path"));
38536
+ var path60 = __toESM(require("path"));
38070
38537
  init_logger();
38071
38538
  init_errors();
38072
38539
  init_fs();
@@ -38102,9 +38569,9 @@ async function runSubsystemAdd(id, options = {}) {
38102
38569
  updatedAt: now
38103
38570
  };
38104
38571
  createChainedSubsystem(subsystem, displayName);
38105
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38572
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
38106
38573
  logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
38107
- logger.info(`Scaffolded child project at ${path61.relative(process.cwd(), childDir) || "."}`);
38574
+ logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
38108
38575
  logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
38109
38576
  }
38110
38577
  async function runSubsystemMove(id, options = {}) {
@@ -38127,9 +38594,9 @@ async function runSubsystemExternalize(id, options = {}) {
38127
38594
  throw new WaironError("--project-path (the subproject destination) is required.");
38128
38595
  }
38129
38596
  externalizeSubsystem(id, options.projectPath);
38130
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38597
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
38131
38598
  logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
38132
- logger.info(`Moved its specs into ${path61.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
38599
+ logger.info(`Moved its specs into ${path60.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
38133
38600
  logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
38134
38601
  }
38135
38602
  async function runSubsystemInternalize(id) {
@@ -38168,8 +38635,64 @@ program.command("generate").description("Generate agent output files from the sp
38168
38635
  dryRun: opts.dryRun
38169
38636
  });
38170
38637
  });
38638
+ async function runLock2(options) {
38639
+ assertProjectInitialized();
38640
+ if (!pathExists(AI_PATHS.specsSystem())) {
38641
+ logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
38642
+ process.exit(1);
38643
+ }
38644
+ const projectConfig = loadProjectConfig();
38645
+ logger.info("Analyzing and validating specifications in-memory...");
38646
+ const dry = validateAsComplete({
38647
+ rules: projectConfig.rules,
38648
+ projectType: projectConfig.projectType,
38649
+ scopeSubsystem: options.subsystem,
38650
+ recursive: options.recursive ?? true
38651
+ });
38652
+ const errors = dry.issues.filter((i) => i.severity === "error");
38653
+ if (errors.length > 0) {
38654
+ logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
38655
+ let errorCount = 0;
38656
+ const MAX_PRINT = 100;
38657
+ let skippedErrors = 0;
38658
+ for (const i of errors) {
38659
+ if (errorCount < MAX_PRINT) {
38660
+ logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
38661
+ errorCount++;
38662
+ } else {
38663
+ skippedErrors++;
38664
+ }
38665
+ }
38666
+ if (skippedErrors > 0) {
38667
+ logger.error(`... and ${skippedErrors} more error(s) omitted.`);
38668
+ }
38669
+ logger.blank();
38670
+ logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
38671
+ process.exit(1);
38672
+ }
38673
+ logger.header("Lock SDD specs");
38674
+ const record2 = await runLock(options, dry);
38675
+ if (!record2) {
38676
+ logger.info("Cancelled. Nothing was changed.");
38677
+ return;
38678
+ }
38679
+ const childPaths = generateChildSnapshots();
38680
+ if (childPaths.length > 0) {
38681
+ logger.blank();
38682
+ logger.success(`Regenerated the family/sibling surfaces into ${childPaths.length} chained child snapshot(s):`);
38683
+ for (const p of childPaths) logger.info(` ${p}`);
38684
+ }
38685
+ logger.blank();
38686
+ await runGenerate({ domain: options.subsystem });
38687
+ logger.blank();
38688
+ logger.success("Specs locked and agent topology generated.");
38689
+ logger.info(`Lock record written (.wai/lock.json): stateId ${record2.stateId.algorithm}:${record2.stateId.digest} \u2014 status ${record2.status}.`);
38690
+ logger.warn(
38691
+ "Restart any running AI agent sessions (Claude Code / Antigravity / Codex) so the newly generated implementer agents load \u2014 they are not picked up mid-session."
38692
+ );
38693
+ }
38171
38694
  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").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) => {
38172
- await runLock({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
38695
+ await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
38173
38696
  });
38174
38697
  program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
38175
38698
  await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
@@ -38285,7 +38808,7 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
38285
38808
  program.command("produce <target>").description("project the local project's specs to a producer target (notion | miro)").option("--page <id>", "parent page/board id in the target").option("--token <token>", "integration token (else env, else interactive prompt)").action(async (target, opts) => {
38286
38809
  await runProduce(target, { page: opts.page, token: opts.token });
38287
38810
  });
38288
- program.command("surface <action>").description("public surface exchange: export | import | list | generate-children").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
38811
+ program.command("surface <action>").description("public surface exchange: export | import | list | generate-children | externals").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
38289
38812
  await runSurface(action, {
38290
38813
  audience: opts.audience,
38291
38814
  format: opts.format,