@tendrilapp/cli 0.1.14 → 0.1.16

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/tendril.js CHANGED
@@ -996,7 +996,7 @@ function sessionStatus(setDir) {
996
996
  const manifest = loadManifest(setDir);
997
997
  const reps = manifest.reps.map((rep) => {
998
998
  const recorded = RECORD_TOOLS.filter((t) => existsSync(path.join(setDir, rep.slug, `${t}.json`)));
999
- const missing = requiredToolsFor(manifest, rep.slug).filter((t) => !recorded.includes(t));
999
+ const missing = byProtocolOrder(requiredToolsFor(manifest, rep.slug).filter((t) => !recorded.includes(t)));
1000
1000
  return { slug: rep.slug, nodeId: rep.nodeId, recorded, missing };
1001
1001
  });
1002
1002
  return { reps, complete: reps.every((r) => r.missing.length === 0) };
@@ -1066,7 +1066,7 @@ function ingestAsset(setDir, slug, name, content) {
1066
1066
  mkdirSync(path.dirname(file), { recursive: true });
1067
1067
  writeFileSync(file, content);
1068
1068
  }
1069
- var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath;
1069
+ var RECORD_TOOLS, INTERIOR_TOOL, REQUIRED_TOOLS, SessionManifestSchema, manifestPath, PROTOCOL_ORDER, byProtocolOrder;
1070
1070
  var init_session = __esm({
1071
1071
  "packages/figma/src/recording/session.ts"() {
1072
1072
  "use strict";
@@ -1108,6 +1108,8 @@ var init_session = __esm({
1108
1108
  roles: z4.unknown().optional()
1109
1109
  });
1110
1110
  manifestPath = (setDir) => path.join(setDir, "recording-set.json");
1111
+ PROTOCOL_ORDER = ["get_metadata", "get_design_context", "get_screenshot", "get_variable_defs", "get_metadata_interior"];
1112
+ byProtocolOrder = (tools) => [...tools].sort((a, b) => PROTOCOL_ORDER.indexOf(a) - PROTOCOL_ORDER.indexOf(b));
1111
1113
  }
1112
1114
  });
1113
1115
 
@@ -1212,8 +1214,8 @@ var init_src = __esm({
1212
1214
  function variableNameToPath(name) {
1213
1215
  return name.split("/").map(canonicalCssIdentPart).filter((part) => part.length > 0);
1214
1216
  }
1215
- function tokenPathToCssVar(path34) {
1216
- return `--${path34.join("-")}`;
1217
+ function tokenPathToCssVar(path35) {
1218
+ return `--${path35.join("-")}`;
1217
1219
  }
1218
1220
  function toDtcgToken(variable, defaultMode) {
1219
1221
  const modes = Object.keys(variable.valuesByMode);
@@ -1257,11 +1259,11 @@ function toDtcgToken(variable, defaultMode) {
1257
1259
  }
1258
1260
  function mapVariablesToDtcg(variables, defaultMode = "light") {
1259
1261
  const entries = variables.map((variable) => {
1260
- const path34 = variableNameToPath(variable.name);
1261
- if (path34.length === 0) {
1262
+ const path35 = variableNameToPath(variable.name);
1263
+ if (path35.length === 0) {
1262
1264
  throw new DtcgMappingError("empty-name", `Figma variable ${variable.id} has an empty name`);
1263
1265
  }
1264
- return { variable, path: path34 };
1266
+ return { variable, path: path35 };
1265
1267
  });
1266
1268
  const groupPrefixes = /* @__PURE__ */ new Set();
1267
1269
  for (const e of entries) {
@@ -1282,21 +1284,21 @@ function mapVariablesToDtcg(variables, defaultMode = "light") {
1282
1284
  }
1283
1285
  const tokens = {};
1284
1286
  const flat = [];
1285
- for (const { variable, path: path34 } of entries) {
1287
+ for (const { variable, path: path35 } of entries) {
1286
1288
  const token = toDtcgToken(variable, defaultMode);
1287
1289
  let group = tokens;
1288
- for (const segment of path34.slice(0, -1)) {
1290
+ for (const segment of path35.slice(0, -1)) {
1289
1291
  const existing = group[segment];
1290
1292
  group = existing ?? (group[segment] = {});
1291
1293
  }
1292
- const leaf = path34[path34.length - 1];
1294
+ const leaf = path35[path35.length - 1];
1293
1295
  if (group[leaf] !== void 0) {
1294
- throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path34.join(".")}" (variable ${variable.id})`);
1296
+ throw new DtcgMappingError("duplicate-path", `Duplicate token path "${path35.join(".")}" (variable ${variable.id})`);
1295
1297
  }
1296
1298
  group[leaf] = token;
1297
1299
  flat.push({
1298
- path: path34.join("."),
1299
- cssVar: tokenPathToCssVar(path34),
1300
+ path: path35.join("."),
1301
+ cssVar: tokenPathToCssVar(path35),
1300
1302
  type: token.$type,
1301
1303
  value: token.$value
1302
1304
  });
@@ -1485,9 +1487,9 @@ function boundId(value) {
1485
1487
  return isObject(value) && typeof value["boundVariableId"] === "string" ? value["boundVariableId"] : void 0;
1486
1488
  }
1487
1489
  function resolveBinding(ctx, id) {
1488
- const path34 = ctx.pathById.get(id);
1489
- if (path34 === void 0) ctx.unresolved.add(id);
1490
- return path34;
1490
+ const path35 = ctx.pathById.get(id);
1491
+ if (path35 === void 0) ctx.unresolved.add(id);
1492
+ return path35;
1491
1493
  }
1492
1494
  function parseVariantProps(name) {
1493
1495
  if (!name.includes("=")) return void 0;
@@ -1522,8 +1524,8 @@ function walk(ctx, raw) {
1522
1524
  if (!isObject(paint) || paint["visible"] === false) continue;
1523
1525
  const id = boundId(paint);
1524
1526
  if (id !== void 0) {
1525
- const path34 = resolveBinding(ctx, id);
1526
- if (path34 !== void 0) tokens.add(path34);
1527
+ const path35 = resolveBinding(ctx, id);
1528
+ if (path35 !== void 0) tokens.add(path35);
1527
1529
  } else if (typeof paint["color"] === "string") {
1528
1530
  ctx.hardcoded.push({ node: name, property, value: paint["color"] });
1529
1531
  }
@@ -1531,8 +1533,8 @@ function walk(ctx, raw) {
1531
1533
  }
1532
1534
  const radiusId = boundId(raw["cornerRadius"]);
1533
1535
  if (radiusId !== void 0) {
1534
- const path34 = resolveBinding(ctx, radiusId);
1535
- if (path34 !== void 0) tokens.add(path34);
1536
+ const path35 = resolveBinding(ctx, radiusId);
1537
+ if (path35 !== void 0) tokens.add(path35);
1536
1538
  } else if (typeof raw["cornerRadius"] === "number" && raw["cornerRadius"] !== 0) {
1537
1539
  ctx.hardcoded.push({ node: name, property: "border-radius", value: `${raw["cornerRadius"]}px` });
1538
1540
  }
@@ -1542,10 +1544,10 @@ function walk(ctx, raw) {
1542
1544
  layout = { mode: layoutMode === "HORIZONTAL" ? "flex-row" : "flex-column" };
1543
1545
  const gapId = boundId(raw["itemSpacing"]);
1544
1546
  if (gapId !== void 0) {
1545
- const path34 = resolveBinding(ctx, gapId);
1546
- if (path34 !== void 0) {
1547
- layout.gap = path34;
1548
- tokens.add(path34);
1547
+ const path35 = resolveBinding(ctx, gapId);
1548
+ if (path35 !== void 0) {
1549
+ layout.gap = path35;
1550
+ tokens.add(path35);
1549
1551
  }
1550
1552
  } else if (typeof raw["itemSpacing"] === "number" && raw["itemSpacing"] !== 0) {
1551
1553
  ctx.hardcoded.push({ node: name, property: "gap", value: `${raw["itemSpacing"]}px` });
@@ -1554,10 +1556,10 @@ function walk(ctx, raw) {
1554
1556
  for (const field of PADDING_FIELDS) {
1555
1557
  const id = boundId(raw[field]);
1556
1558
  if (id !== void 0) {
1557
- const path34 = resolveBinding(ctx, id);
1558
- if (path34 !== void 0) {
1559
- paddingPaths.push(path34);
1560
- tokens.add(path34);
1559
+ const path35 = resolveBinding(ctx, id);
1560
+ if (path35 !== void 0) {
1561
+ paddingPaths.push(path35);
1562
+ tokens.add(path35);
1561
1563
  }
1562
1564
  } else if (typeof raw[field] === "number" && raw[field] !== 0) {
1563
1565
  ctx.hardcoded.push({ node: name, property: field, value: `${raw[field]}px` });
@@ -1977,7 +1979,13 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2) {
1977
1979
  file: fileLabel,
1978
1980
  line: w.line,
1979
1981
  property: w.rule,
1980
- message: w.text
1982
+ // Run 7: a single-pose token map left 15 of 20 colors with no
1983
+ // token to reference, and 22 strict-value findings read as
1984
+ // must-fix while the brief forbade inventing names — unfixable
1985
+ // noise in the highest-attention output. The finding stays (a
1986
+ // matching token SHOULD be referenced when one exists); the
1987
+ // message now carries the escape hatch.
1988
+ message: w.rule === "scale-unlimited/declaration-strict-value" ? `${w.text} \u2014 ADVISORY: reference the design token if the kit provides one; if it does not, the literal is CORRECT \u2014 never invent a token name (the recorded token map captures one pose's variables and is known-partial)` : w.text
1981
1989
  }))
1982
1990
  );
1983
1991
  const ownDefs = /* @__PURE__ */ new Set();
@@ -2324,6 +2332,23 @@ function detectBackdrop(png, opts = {}) {
2324
2332
  const { modal, consensus } = ringConsensus(png);
2325
2333
  if (consensus < 0.9) return [255, 255, 255];
2326
2334
  if (Math.min(...modal) < 200 && opts.trustDark !== true) return [255, 255, 255];
2335
+ const decoded = PNG.sync.read(Buffer.from(png));
2336
+ if (decoded.width >= 4 && decoded.height >= 4) {
2337
+ const blocks = cornerBlocks(decoded, 2);
2338
+ const first = blocks[0];
2339
+ const cornersAgree = blocks.every((c) => Math.abs(c[0] - first[0]) + Math.abs(c[1] - first[1]) + Math.abs(c[2] - first[2]) <= INK_DELTA);
2340
+ const cornersDisagreeWithRing = Math.abs(first[0] - modal[0]) + Math.abs(first[1] - modal[1]) + Math.abs(first[2] - modal[2]) > INK_DELTA;
2341
+ if (cornersAgree && cornersDisagreeWithRing) {
2342
+ const extremes = cornerBlocks(decoded, 1);
2343
+ const value = [
2344
+ Math.round(extremes.reduce((a, c) => a + c[0], 0) / 4),
2345
+ Math.round(extremes.reduce((a, c) => a + c[1], 0) / 4),
2346
+ Math.round(extremes.reduce((a, c) => a + c[2], 0) / 4)
2347
+ ];
2348
+ if (Math.min(...value) < 200 && opts.trustDark !== true) return [255, 255, 255];
2349
+ return value;
2350
+ }
2351
+ }
2327
2352
  return modal;
2328
2353
  }
2329
2354
  function pngDimensions(png) {
@@ -5118,6 +5143,107 @@ var init_output = __esm({
5118
5143
  }
5119
5144
  });
5120
5145
 
5146
+ // packages/cli/src/entitlement.ts
5147
+ import { chmodSync, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
5148
+ import crypto from "node:crypto";
5149
+ import os3 from "node:os";
5150
+ import path21 from "node:path";
5151
+ function entitlementPath() {
5152
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path21.join(os3.homedir(), ".tendril", "entitlement.json");
5153
+ }
5154
+ function readStoredEntitlement(file = entitlementPath()) {
5155
+ if (!existsSync16(file)) return void 0;
5156
+ try {
5157
+ const parsed = JSON.parse(readFileSync13(file, "utf8"));
5158
+ if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
5159
+ return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
5160
+ } catch {
5161
+ return void 0;
5162
+ }
5163
+ }
5164
+ function writeStoredEntitlement(stored, file = entitlementPath()) {
5165
+ mkdirSync4(path21.dirname(file), { recursive: true });
5166
+ writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5167
+ `);
5168
+ chmodSync(file, 384);
5169
+ }
5170
+ function parseEntitlementToken(token) {
5171
+ if (!token.startsWith(ENT_PREFIX)) return { error: "not a tendril entitlement token" };
5172
+ const parts = token.slice(ENT_PREFIX.length).split(".");
5173
+ if (parts.length !== 2 || parts[0] === "" || parts[1] === "") return { error: "malformed token (expected payload.signature)" };
5174
+ let claims;
5175
+ try {
5176
+ claims = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
5177
+ } catch {
5178
+ return { error: "payload is not valid JSON" };
5179
+ }
5180
+ if (typeof claims.sub !== "string" || typeof claims.plan !== "string" || typeof claims.iat !== "number" || typeof claims.exp !== "number" || typeof claims.kid !== "string") {
5181
+ return { error: "payload is missing required claims" };
5182
+ }
5183
+ return { claims, signedData: b64urlDecode(parts[0]), signature: b64urlDecode(parts[1]) };
5184
+ }
5185
+ function checkEntitlement(opts = {}) {
5186
+ const keys = opts.keys ?? PUBLIC_KEYS;
5187
+ if (Object.keys(keys).length === 0) return { ok: true, mode: "pre-launch" };
5188
+ const now = opts.now ?? Date.now();
5189
+ const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
5190
+ if (stored === void 0) {
5191
+ return { ok: false, code: "entitlement-required", error: "no entitlement on this machine \u2014 record and generate need an active Tendril plan (verify stays free, always)", remediation: ACTIVATE_REMEDIATION };
5192
+ }
5193
+ const parsed = parseEntitlementToken(stored.token);
5194
+ if ("error" in parsed) {
5195
+ return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: ACTIVATE_REMEDIATION };
5196
+ }
5197
+ const pem = keys[parsed.claims.kid];
5198
+ const valid = pem !== void 0 && (() => {
5199
+ try {
5200
+ return crypto.verify(null, parsed.signedData, crypto.createPublicKey(pem), parsed.signature);
5201
+ } catch {
5202
+ return false;
5203
+ }
5204
+ })();
5205
+ if (!valid) {
5206
+ return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: ACTIVATE_REMEDIATION };
5207
+ }
5208
+ if (now > parsed.claims.exp + TOLERANCE_MS) {
5209
+ return {
5210
+ ok: false,
5211
+ code: "entitlement-expired",
5212
+ error: "entitlement expired (and the offline tolerance window has passed)",
5213
+ remediation: `Reconnect and ${ACTIVATE_REMEDIATION}`
5214
+ };
5215
+ }
5216
+ if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
5217
+ return {
5218
+ ok: false,
5219
+ code: "entitlement-clock",
5220
+ error: "system clock sits more than a day before the last entitlement refresh",
5221
+ remediation: `Fix the system clock, then ${ACTIVATE_REMEDIATION}`
5222
+ };
5223
+ }
5224
+ return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
5225
+ }
5226
+ function requireEntitlement(flags) {
5227
+ const status = checkEntitlement();
5228
+ if (!status.ok) {
5229
+ fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
5230
+ }
5231
+ }
5232
+ var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, ACTIVATE_REMEDIATION;
5233
+ var init_entitlement = __esm({
5234
+ "packages/cli/src/entitlement.ts"() {
5235
+ "use strict";
5236
+ init_src3();
5237
+ init_output();
5238
+ ENT_PREFIX = "tendril-ent.v1.";
5239
+ PUBLIC_KEYS = {};
5240
+ TOLERANCE_MS = 24 * 60 * 60 * 1e3;
5241
+ CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
5242
+ b64urlDecode = (s) => Buffer.from(s, "base64url");
5243
+ ACTIVATE_REMEDIATION = "Run `tendril activate` in your terminal (a browser approval \u2014 never paste license material into an agent chat).";
5244
+ }
5245
+ });
5246
+
5121
5247
  // packages/llm/src/model-config.ts
5122
5248
  import { z as z6 } from "zod";
5123
5249
  function resolveModel(config, requestedId) {
@@ -6309,6 +6435,100 @@ var init_src6 = __esm({
6309
6435
  }
6310
6436
  });
6311
6437
 
6438
+ // packages/cli/src/commands/activate.ts
6439
+ var activate_exports = {};
6440
+ __export(activate_exports, {
6441
+ ENTITLEMENT_SERVICE_URL: () => ENTITLEMENT_SERVICE_URL,
6442
+ runActivate: () => runActivate
6443
+ });
6444
+ async function runActivate(flags) {
6445
+ const base = flags.serviceUrl ?? ENTITLEMENT_SERVICE_URL;
6446
+ if (base === void 0) {
6447
+ fail(flags, ExitCode.Auth, {
6448
+ error: "the Tendril entitlement service is not live yet (pre-launch build) \u2014 there is nothing to activate against",
6449
+ code: "entitlement-service-unavailable",
6450
+ remediation: "Nothing to do: pre-launch builds run record/generate without activation. This command becomes meaningful at launch."
6451
+ });
6452
+ }
6453
+ let device;
6454
+ try {
6455
+ const res = await fetch(new URL("/v1/device/code", base), { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
6456
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6457
+ device = await res.json();
6458
+ if (typeof device.deviceCode !== "string" || typeof device.userCode !== "string" || typeof device.verificationUri !== "string") throw new Error("malformed device-code response");
6459
+ } catch (err) {
6460
+ fail(flags, ExitCode.Auth, {
6461
+ error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
6462
+ code: "entitlement-service-unreachable",
6463
+ remediation: "Check your connection and retry `tendril activate`."
6464
+ });
6465
+ }
6466
+ process.stderr.write(`To activate Tendril, visit:
6467
+
6468
+ ${device.verificationUri}
6469
+
6470
+ and enter the code: ${device.userCode}
6471
+
6472
+ Waiting for approval\u2026
6473
+ `);
6474
+ const interval = Math.max(1e3, device.intervalMs ?? 5e3);
6475
+ const deadline = Date.now() + (device.expiresInMs ?? 10 * 60 * 1e3);
6476
+ let token;
6477
+ while (Date.now() < deadline) {
6478
+ await new Promise((r) => setTimeout(r, interval));
6479
+ const res = await fetch(new URL("/v1/device/token", base), {
6480
+ method: "POST",
6481
+ headers: { "content-type": "application/json" },
6482
+ body: JSON.stringify({ deviceCode: device.deviceCode })
6483
+ }).catch(() => void 0);
6484
+ if (res === void 0) continue;
6485
+ if (res.status === 428) continue;
6486
+ if (!res.ok) {
6487
+ fail(flags, ExitCode.Auth, {
6488
+ error: `activation was not approved (HTTP ${res.status})`,
6489
+ code: "entitlement-denied",
6490
+ remediation: "Retry `tendril activate`; if it persists, check the account's plan in the portal."
6491
+ });
6492
+ }
6493
+ token = (await res.json()).token;
6494
+ break;
6495
+ }
6496
+ if (token === void 0) {
6497
+ fail(flags, ExitCode.Auth, {
6498
+ error: "activation timed out before the browser approval arrived",
6499
+ code: "entitlement-timeout",
6500
+ remediation: "Run `tendril activate` again and complete the browser step within the shown window."
6501
+ });
6502
+ }
6503
+ const parsed = parseEntitlementToken(token);
6504
+ if ("error" in parsed) {
6505
+ fail(flags, ExitCode.Auth, {
6506
+ error: `service returned an unusable token: ${parsed.error}`,
6507
+ code: "entitlement-invalid",
6508
+ remediation: "Retry `tendril activate`; report this if it persists \u2014 it is a service-side fault."
6509
+ });
6510
+ }
6511
+ if (Object.keys(PUBLIC_KEYS).length > 0) {
6512
+ const status = checkEntitlement({ now: Date.now(), stored: { token, lastRefreshAt: Date.now() } });
6513
+ if (!status.ok) fail(flags, ExitCode.Auth, { error: `service returned a token this build cannot verify: ${status.error}`, code: status.code, remediation: status.remediation });
6514
+ }
6515
+ writeStoredEntitlement({ token, lastRefreshAt: Date.now() });
6516
+ emitData(flags, { activated: true, plan: parsed.claims.plan, sub: parsed.claims.sub, expiresAt: new Date(parsed.claims.exp).toISOString(), storedAt: entitlementPath() }, () => {
6517
+ process.stdout.write(`activated: plan "${parsed.claims.plan}" until ${new Date(parsed.claims.exp).toISOString().slice(0, 10)} (stored at ${entitlementPath()})
6518
+ `);
6519
+ });
6520
+ }
6521
+ var ENTITLEMENT_SERVICE_URL;
6522
+ var init_activate = __esm({
6523
+ "packages/cli/src/commands/activate.ts"() {
6524
+ "use strict";
6525
+ init_src3();
6526
+ init_entitlement();
6527
+ init_output();
6528
+ ENTITLEMENT_SERVICE_URL = void 0;
6529
+ }
6530
+ });
6531
+
6312
6532
  // packages/cli/src/commands/record.ts
6313
6533
  var record_exports = {};
6314
6534
  __export(record_exports, {
@@ -6325,11 +6545,11 @@ __export(record_exports, {
6325
6545
  runRecordPlan: () => runRecordPlan,
6326
6546
  runRecordStatus: () => runRecordStatus
6327
6547
  });
6328
- import { existsSync as existsSync18, readFileSync as readFileSync15, readdirSync as readdirSync4 } from "node:fs";
6329
- import path24 from "node:path";
6330
- import { writeFileSync as writeFileSync8 } from "node:fs";
6548
+ import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
6549
+ import path25 from "node:path";
6550
+ import { writeFileSync as writeFileSync9 } from "node:fs";
6331
6551
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
6332
- const env = JSON.parse(readFileSync15(file, "utf8"));
6552
+ const env = JSON.parse(readFileSync16(file, "utf8"));
6333
6553
  const text = env.content.map((c) => c.text ?? "").join("\n");
6334
6554
  const symbols = [];
6335
6555
  const walk2 = (node, ancestor) => {
@@ -6359,6 +6579,7 @@ function instanceLeads(text) {
6359
6579
  return [...seen.entries()].map(([name, nodeId]) => ({ name, nodeId }));
6360
6580
  }
6361
6581
  function runRecordPlan(opts) {
6582
+ requireEntitlement(opts);
6362
6583
  const defaults = {};
6363
6584
  for (const spec of opts.defaultSpecs ?? []) {
6364
6585
  const eq = spec.indexOf("=");
@@ -6376,7 +6597,7 @@ function runRecordPlan(opts) {
6376
6597
  for (const spec of opts.metadataFiles) {
6377
6598
  const [file, frame] = spec.split("@");
6378
6599
  try {
6379
- const parsed = symbolsFromMetadataEnvelope(path24.resolve(file), frame);
6600
+ const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
6380
6601
  symbols.push(...parsed.symbols);
6381
6602
  if (parsed.truncated) metadataTruncated = true;
6382
6603
  } catch (err) {
@@ -6410,7 +6631,7 @@ function runRecordPlan(opts) {
6410
6631
  const leads = opts.metadataFiles.flatMap((spec) => {
6411
6632
  const [file] = spec.split("@");
6412
6633
  try {
6413
- const env = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
6634
+ const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
6414
6635
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
6415
6636
  } catch {
6416
6637
  return [];
@@ -6467,6 +6688,10 @@ function runRecordPlan(opts) {
6467
6688
  ...toppedUp !== void 0 ? { toppedUp } : {},
6468
6689
  notRecorded: manifest.notRecorded ?? null,
6469
6690
  figmaCallEstimate: { reps: manifest.reps.length, calls: `~${callLow}\u2013${callHigh}` },
6691
+ // Cold-start fix (run 7): the first instruction rides the plan
6692
+ // response, so record_next is never needed to begin — it exists
6693
+ // only for resuming.
6694
+ next: nextPayload(opts.setDir),
6470
6695
  ...toConfirm.length > 0 ? {
6471
6696
  defaultsToConfirm: {
6472
6697
  instruction: "HEURISTIC defaults: the designer named no literal Default and no override was given, so the code guessed the component's zero point \u2014 the pose an empty-props mount shows and the pose behaviour checks aim at. When a USER is present, ask each question below BEFORE recording; if they pick a different value, re-run this exact plan command with --default <Axis>=<Value> (allowed until the first envelope is ingested; frozen with the recording after). Non-interactive: proceed with the resolved values and state them in your report.",
@@ -6511,7 +6736,7 @@ function nextPayload(setDir) {
6511
6736
  const instruction = nextInstruction(setDir);
6512
6737
  const status = sessionStatus(setDir);
6513
6738
  const progress = { recordedReps: status.reps.filter((x) => x.missing.length === 0).length, totalReps: status.reps.length };
6514
- if (instruction === null && !existsSync18(path24.join(setDir, "get_variable_defs.json"))) {
6739
+ if (instruction === null && !existsSync19(path25.join(setDir, "get_variable_defs.json"))) {
6515
6740
  const manifest = loadManifest(setDir);
6516
6741
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
6517
6742
  return { slug: "__set__", nodeId: frameNode, tool: "get_variable_defs", note: `SET-LEVEL: call get_variable_defs on the component frame and ingest with --rep __set__. ${ENVELOPE_HELP}`, progress };
@@ -6602,7 +6827,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6602
6827
  const skipped = [];
6603
6828
  const failed = [];
6604
6829
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
6605
- if (existsSync18(path24.join(setDir, rep, name))) {
6830
+ if (existsSync19(path25.join(setDir, rep, name))) {
6606
6831
  skipped.push(name);
6607
6832
  continue;
6608
6833
  }
@@ -6624,16 +6849,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6624
6849
  }
6625
6850
  function rawEnvelopeFromFile(file, parts) {
6626
6851
  if (parts) {
6627
- const blocks = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
6852
+ const blocks = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
6628
6853
  if (!Array.isArray(blocks) || blocks.length === 0 || blocks.some((p) => typeof p !== "string")) throw new Error("parts file must be a non-empty JSON array of strings");
6629
6854
  return { content: blocks.map((text) => ({ type: "text", text })) };
6630
6855
  }
6631
- return { content: [{ type: "text", text: readFileSync15(path24.resolve(file), "utf8") }] };
6856
+ return { content: [{ type: "text", text: readFileSync16(path25.resolve(file), "utf8") }] };
6632
6857
  }
6633
6858
  async function runRecordIngest(opts) {
6634
6859
  let payload;
6635
6860
  try {
6636
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6861
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path25.resolve(opts.file), "utf8"));
6637
6862
  } catch (err) {
6638
6863
  fail(opts, ExitCode.InputValidation, {
6639
6864
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -6657,7 +6882,7 @@ async function runRecordIngest(opts) {
6657
6882
  remediation: "Save the get_variable_defs response verbatim as a text envelope."
6658
6883
  });
6659
6884
  }
6660
- writeFileSync8(path24.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
6885
+ writeFileSync9(path25.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
6661
6886
  `);
6662
6887
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
6663
6888
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -6753,8 +6978,8 @@ async function runRecordIngestRep(opts) {
6753
6978
  }
6754
6979
  function runRecordAsset(opts) {
6755
6980
  if (opts.dir !== void 0) {
6756
- const dir = path24.resolve(opts.dir);
6757
- const names = readdirSync4(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
6981
+ const dir = path25.resolve(opts.dir);
6982
+ const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
6758
6983
  if (names.length === 0) {
6759
6984
  fail(opts, ExitCode.InputValidation, {
6760
6985
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -6765,7 +6990,7 @@ function runRecordAsset(opts) {
6765
6990
  const ingested = [];
6766
6991
  try {
6767
6992
  for (const name of names) {
6768
- ingestAsset(opts.setDir, opts.rep, name, readFileSync15(path24.join(dir, name)));
6993
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path25.join(dir, name)));
6769
6994
  ingested.push(name);
6770
6995
  }
6771
6996
  } catch (err) {
@@ -6789,7 +7014,7 @@ function runRecordAsset(opts) {
6789
7014
  });
6790
7015
  }
6791
7016
  try {
6792
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync15(path24.resolve(opts.file)));
7017
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path25.resolve(opts.file)));
6793
7018
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
6794
7019
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
6795
7020
  `);
@@ -6817,7 +7042,7 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
6817
7042
  function runRecordFinish(opts) {
6818
7043
  const manifest = loadManifest(opts.setDir);
6819
7044
  const derived = deriveRoles(opts.setDir, manifest);
6820
- const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(readFileSync15(path24.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
7045
+ const roles = opts.rolesFile !== void 0 ? { ...JSON.parse(readFileSync16(path25.resolve(opts.rolesFile), "utf8")), humanOverride: true } : { main: derived.main, parts: derived.parts, external: derived.external, humanOverride: false };
6821
7046
  emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
6822
7047
  process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
6823
7048
  `);
@@ -6844,7 +7069,7 @@ function runRecordFinish(opts) {
6844
7069
  });
6845
7070
  }
6846
7071
  const updated = { ...manifest, roles };
6847
- writeFileSync8(path24.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
7072
+ writeFileSync9(path25.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
6848
7073
  `);
6849
7074
  if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
6850
7075
  }
@@ -6855,6 +7080,7 @@ var init_record = __esm({
6855
7080
  init_src3();
6856
7081
  init_src();
6857
7082
  init_output();
7083
+ init_entitlement();
6858
7084
  ENVELOPE_HELP = 'Envelope format: text tools save {"content":[{"type":"text","text":"<VERBATIM response text incl. any Currently-selected-nodes block>"}]}; get_screenshot: do NOT download the image yourself \u2014 pass its image_url to `tendril record fetch` (MCP: tendril_record_fetch), which pulls the bytes to disk directly. That keeps the pixel ground truth out of your context and costs one approval instead of a shell command per asset. Assets are asset-<first-8-hex-of-figma-uuid>.<ext>; their URLs appear as const declarations inside the design-context text and also expire.';
6859
7085
  isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
6860
7086
  }
@@ -7047,8 +7273,8 @@ var init_engine_curated = __esm({
7047
7273
  });
7048
7274
 
7049
7275
  // packages/generate/src/loop.ts
7050
- import { existsSync as existsSync19, mkdirSync as mkdirSync5, readFileSync as readFileSync16, renameSync, writeFileSync as writeFileSync9 } from "node:fs";
7051
- import path25 from "node:path";
7276
+ import { existsSync as existsSync20, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
7277
+ import path26 from "node:path";
7052
7278
  import { z as z11 } from "zod";
7053
7279
  function objective(scores, behaviors) {
7054
7280
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -7079,9 +7305,9 @@ ${preludeLines.join("\n")}` : ""}
7079
7305
  Fix the FAIL configs (region coordinates are in the recorded screenshot's frame; ink<1 means recorded foreground pixels your render does not cover). Do not regress PASS configs. ${mode === "files" ? "Update the files in your candidate directory and re-run the score." : "Reply with the complete corrected files in the same FILE format."}`;
7080
7306
  }
7081
7307
  function archivePriorRun(outDir) {
7082
- if (!existsSync19(path25.join(outDir, "run-log.json")) && !existsSync19(path25.join(outDir, "loop-state.json"))) return void 0;
7308
+ if (!existsSync20(path26.join(outDir, "run-log.json")) && !existsSync20(path26.join(outDir, "loop-state.json"))) return void 0;
7083
7309
  let n = 1;
7084
- while (existsSync19(`${outDir}-prev-${n}`)) n += 1;
7310
+ while (existsSync20(`${outDir}-prev-${n}`)) n += 1;
7085
7311
  renameSync(outDir, `${outDir}-prev-${n}`);
7086
7312
  return `${outDir}-prev-${n}`;
7087
7313
  }
@@ -7090,14 +7316,14 @@ async function runEngineLoop(opts) {
7090
7316
  const plateau = opts.plateau ?? 2;
7091
7317
  const progress = opts.onProgress ?? (() => {
7092
7318
  });
7093
- const statePath = path25.join(opts.outDir, "loop-state.json");
7094
- const resuming = opts.resume === true && existsSync19(statePath);
7319
+ const statePath = path26.join(opts.outDir, "loop-state.json");
7320
+ const resuming = opts.resume === true && existsSync20(statePath);
7095
7321
  if (!resuming) {
7096
7322
  const archived = archivePriorRun(opts.outDir);
7097
7323
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
7098
7324
  }
7099
- mkdirSync5(opts.outDir, { recursive: true });
7100
- const scratch = path25.join(opts.outDir, ".candidate");
7325
+ mkdirSync6(opts.outDir, { recursive: true });
7326
+ const scratch = path26.join(opts.outDir, ".candidate");
7101
7327
  let attempts = [];
7102
7328
  let log = [];
7103
7329
  let best;
@@ -7105,7 +7331,7 @@ async function runEngineLoop(opts) {
7105
7331
  let nonAccepted = 0;
7106
7332
  let stopReason = "max-iterations";
7107
7333
  if (resuming) {
7108
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync16(statePath, "utf8")));
7334
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
7109
7335
  attempts = restored.attempts;
7110
7336
  log = restored.iterations;
7111
7337
  spentUsd = restored.spentUsd;
@@ -7120,12 +7346,12 @@ async function runEngineLoop(opts) {
7120
7346
  progress(`resumed: ${log.length} iteration(s), $${spentUsd.toFixed(3)} spent, best pass=${best?.objective[0] ?? 0}`);
7121
7347
  }
7122
7348
  const persist = () => {
7123
- writeFileSync9(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
7349
+ writeFileSync10(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
7124
7350
  `);
7125
7351
  };
7126
7352
  const writeCandidate = (files) => {
7127
- mkdirSync5(scratch, { recursive: true });
7128
- for (const [name, content] of Object.entries(files)) writeFileSync9(path25.join(scratch, name), content);
7353
+ mkdirSync6(scratch, { recursive: true });
7354
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path26.join(scratch, name), content);
7129
7355
  };
7130
7356
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
7131
7357
  writeCandidate(candidate.files);
@@ -7183,8 +7409,8 @@ async function runEngineLoop(opts) {
7183
7409
  const usd = candidate.usage?.usd ?? 0;
7184
7410
  spentUsd += usd;
7185
7411
  if (candidate.raw !== void 0) {
7186
- mkdirSync5(path25.join(opts.outDir, "responses"), { recursive: true });
7187
- writeFileSync9(path25.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
7412
+ mkdirSync6(path26.join(opts.outDir, "responses"), { recursive: true });
7413
+ writeFileSync10(path26.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
7188
7414
  }
7189
7415
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
7190
7416
  const finish = candidate.usage?.finishReason ?? "?";
@@ -7210,10 +7436,10 @@ async function runEngineLoop(opts) {
7210
7436
  }
7211
7437
  }
7212
7438
  }
7213
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(path25.join(opts.outDir, name), content);
7439
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path26.join(opts.outDir, name), content);
7214
7440
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
7215
- writeFileSync9(
7216
- path25.join(opts.outDir, "run-log.json"),
7441
+ writeFileSync10(
7442
+ path26.join(opts.outDir, "run-log.json"),
7217
7443
  `${JSON.stringify(
7218
7444
  {
7219
7445
  ...opts.meta,
@@ -7280,8 +7506,8 @@ var init_loop2 = __esm({
7280
7506
  });
7281
7507
 
7282
7508
  // packages/generate/src/brief.ts
7283
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
7284
- import path26 from "node:path";
7509
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "node:fs";
7510
+ import path27 from "node:path";
7285
7511
  function singleAxes2(name) {
7286
7512
  const parsed = parseVariantAxes(name);
7287
7513
  if (parsed === void 0) return void 0;
@@ -7407,8 +7633,8 @@ function authorComponentApi(opts) {
7407
7633
  configs.push({ rep: p.slug, component: componentIdent, props: {} });
7408
7634
  }
7409
7635
  if (inexpressible.length > 0) throw new PoseCompletenessError(inexpressible);
7410
- if (opts.dismissible === true) {
7411
- props.push({ name: "onDismiss", kind: "callback" });
7636
+ if (opts.dismissible !== void 0) {
7637
+ props.push({ name: "onDismiss", kind: "callback", default: opts.dismissible });
7412
7638
  }
7413
7639
  const entry = `${componentIdent}.tsx`;
7414
7640
  const apiPin = {
@@ -7417,13 +7643,15 @@ function authorComponentApi(opts) {
7417
7643
  name: pr.name,
7418
7644
  type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.kind === "callback" ? "() => void" : pr.values.map((v) => `"${v}"`).join(" | "),
7419
7645
  required: false,
7420
- ...pr.default !== void 0 ? { default: pr.default } : {}
7646
+ // A callback's `default` field carries detection EVIDENCE for the
7647
+ // prop line, not a value — it never enters the pin.
7648
+ ...pr.default !== void 0 && pr.kind !== "callback" ? { default: pr.default } : {}
7421
7649
  })),
7422
7650
  forcedStates,
7423
7651
  poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
7424
7652
  };
7425
7653
  const propLines = props.map(
7426
- (pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 fires on the recorded affordance; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
7654
+ (pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 recorded evidence: ${pr.default ?? "the recorded affordance"}; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
7427
7655
  );
7428
7656
  const provided = opts.fonts ?? [];
7429
7657
  const recorded = opts.recordedFonts ?? [];
@@ -7496,7 +7724,7 @@ function authorBehaviors(api, extras = {}) {
7496
7724
  behaviors.push({
7497
7725
  id: `content-prop-renders(${sentinel.prop})`,
7498
7726
  config: sentinel.config,
7499
- props: { [sentinel.prop]: marker },
7727
+ props: { ...sentinel.props ?? {}, [sentinel.prop]: marker },
7500
7728
  steps: [{ assertTextVisible: marker }]
7501
7729
  });
7502
7730
  }
@@ -7514,15 +7742,18 @@ function authorBehaviors(api, extras = {}) {
7514
7742
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
7515
7743
  }
7516
7744
  function envelopeText(file) {
7517
- return envelopeFirstTextPart(JSON.parse(readFileSync17(file, "utf8")));
7745
+ return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
7518
7746
  }
7519
7747
  function dismissEvidence(setDir, repSlugs) {
7520
7748
  for (const slug of repSlugs) {
7521
- const f = path26.join(setDir, slug, "get_design_context.json");
7522
- if (!existsSync20(f)) continue;
7523
- for (const m of envelopeText(f).matchAll(/data-name="([^"]+)"/g)) {
7749
+ const f = path27.join(setDir, slug, "get_design_context.json");
7750
+ if (!existsSync21(f)) continue;
7751
+ const text = envelopeText(f);
7752
+ const propHit = /[{,]\s*(\w*dismiss\w*)\s*=\s*(?:true|false)\b/i.exec(text) ?? /\b(\w*dismiss\w*)\??\s*:\s*boolean\b/i.exec(text);
7753
+ if (propHit !== null) return `emission prop "${propHit[1]}"`;
7754
+ for (const m of text.matchAll(/data-name="([^"]+)"/g)) {
7524
7755
  const norm = m[1].toLowerCase().replace(/[^a-z0-9]/g, "");
7525
- if (DISMISS_NAMES.has(norm)) return m[1];
7756
+ if (DISMISS_NAMES.has(norm)) return `layer ${JSON.stringify(m[1])}`;
7526
7757
  }
7527
7758
  }
7528
7759
  return void 0;
@@ -7560,13 +7791,13 @@ function recordedFontNeeds(setDir) {
7560
7791
  }
7561
7792
  };
7562
7793
  const manifest = loadManifest(setDir);
7563
- const setDefs = path26.join(setDir, "get_variable_defs.json");
7564
- if (existsSync20(setDefs)) fromDefs(envelopeText(setDefs));
7794
+ const setDefs = path27.join(setDir, "get_variable_defs.json");
7795
+ if (existsSync21(setDefs)) fromDefs(envelopeText(setDefs));
7565
7796
  for (const rep of manifest.reps) {
7566
- const ctx = path26.join(setDir, rep.slug, "get_design_context.json");
7567
- if (existsSync20(ctx)) fromEmission(envelopeText(ctx));
7568
- const defs = path26.join(setDir, rep.slug, "get_variable_defs.json");
7569
- if (existsSync20(defs)) fromDefs(envelopeText(defs));
7797
+ const ctx = path27.join(setDir, rep.slug, "get_design_context.json");
7798
+ if (existsSync21(ctx)) fromEmission(envelopeText(ctx));
7799
+ const defs = path27.join(setDir, rep.slug, "get_variable_defs.json");
7800
+ if (existsSync21(defs)) fromDefs(envelopeText(defs));
7570
7801
  }
7571
7802
  return [...byFamily.entries()].map(([family, paired]) => {
7572
7803
  const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
@@ -7589,8 +7820,8 @@ function recordedTextSlots(setDir, repSlugs) {
7589
7820
  const propRep = [];
7590
7821
  const perRep = [];
7591
7822
  for (const slug of repSlugs) {
7592
- const f = path26.join(setDir, slug, "get_design_context.json");
7593
- if (!existsSync20(f)) continue;
7823
+ const f = path27.join(setDir, slug, "get_design_context.json");
7824
+ if (!existsSync21(f)) continue;
7594
7825
  const code = envelopeText(f);
7595
7826
  const props = /* @__PURE__ */ new Map();
7596
7827
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -7606,12 +7837,17 @@ function recordedTextSlots(setDir, repSlugs) {
7606
7837
  if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
7607
7838
  texts.push(t);
7608
7839
  }
7609
- perRep.push({ slug, texts });
7840
+ for (const m of code.matchAll(/>\{[`'"]([^`'"]+)[`'"]\}</g)) {
7841
+ const t = decodeXmlEntities(m[1]).trim();
7842
+ if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
7843
+ texts.push(t);
7844
+ }
7845
+ perRep.push({ slug, texts, code });
7610
7846
  }
7611
7847
  const axisValuesBySlug = /* @__PURE__ */ new Map();
7612
7848
  for (const slug of repSlugs) {
7613
- const metaFile = path26.join(setDir, slug, "get_metadata.json");
7614
- if (!existsSync20(metaFile)) continue;
7849
+ const metaFile = path27.join(setDir, slug, "get_metadata.json");
7850
+ if (!existsSync21(metaFile)) continue;
7615
7851
  const name = symbolName(envelopeText(metaFile));
7616
7852
  if (name === void 0) continue;
7617
7853
  const values = /* @__PURE__ */ new Set();
@@ -7647,9 +7883,11 @@ function recordedTextSlots(setDir, repSlugs) {
7647
7883
  }
7648
7884
  const visibleIn = perRep.filter((r) => {
7649
7885
  const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
7650
- return r.texts.some((t) => t.includes(v));
7886
+ return r.texts.some((t) => t.includes(v)) || r.texts.join(" ").replace(/\s+/g, " ").includes(v.replace(/\s+/g, " ").trim());
7651
7887
  }).map((r) => r.slug);
7652
- return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn };
7888
+ const refPattern = new RegExp(`\\{\\s*${name}\\s*\\}`);
7889
+ const referencedIn = perRep.filter((r) => refPattern.test(r.code)).map((r) => r.slug);
7890
+ return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn, referencedIn };
7653
7891
  });
7654
7892
  }
7655
7893
  if (perRep.length === 0) return [];
@@ -7696,7 +7934,7 @@ function recordedTextSlots(setDir, repSlugs) {
7696
7934
  usedNames.add(prop);
7697
7935
  const overrides = {};
7698
7936
  for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
7699
- return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()] };
7937
+ return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()], referencedIn: [] };
7700
7938
  });
7701
7939
  }
7702
7940
  function authorTaskFromSet(setDir, opts = {}) {
@@ -7708,8 +7946,8 @@ function authorTaskFromSet(setDir, opts = {}) {
7708
7946
  const poses = [];
7709
7947
  const missing = [];
7710
7948
  for (const rep of manifest.reps) {
7711
- const metaFile = path26.join(setDir, rep.slug, "get_metadata.json");
7712
- if (!existsSync20(metaFile)) {
7949
+ const metaFile = path27.join(setDir, rep.slug, "get_metadata.json");
7950
+ if (!existsSync21(metaFile)) {
7713
7951
  missing.push(rep.slug);
7714
7952
  continue;
7715
7953
  }
@@ -7723,8 +7961,8 @@ function authorTaskFromSet(setDir, opts = {}) {
7723
7961
  if (missing.length > 0) {
7724
7962
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
7725
7963
  }
7726
- const setMeta = path26.join(setDir, "get_metadata.json");
7727
- const latticeNames = manifest.latticeNames ?? (existsSync20(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
7964
+ const setMeta = path27.join(setDir, "get_metadata.json");
7965
+ const latticeNames = manifest.latticeNames ?? (existsSync21(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
7728
7966
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
7729
7967
  const recordedFonts = recordedFontFamilies(setDir);
7730
7968
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -7737,26 +7975,48 @@ function authorTaskFromSet(setDir, opts = {}) {
7737
7975
  ...recordedFonts.length > 0 ? { recordedFonts } : {},
7738
7976
  ...Object.keys(defaults).length > 0 ? { defaults } : {},
7739
7977
  ...textSlots.length > 0 ? { textSlots } : {},
7740
- ...dismissName !== void 0 ? { dismissible: true } : {}
7978
+ ...dismissName !== void 0 ? { dismissible: dismissName } : {}
7741
7979
  });
7742
7980
  const anchorSlug = api.configs.find((c) => Object.keys(c.props).length === 0)?.rep ?? api.configs[0]?.rep;
7743
- const sentinels = textSlots.filter((slot) => !slot.varies && slot.visibleIn.length > 0).map((slot) => {
7981
+ const sentinels = textSlots.filter((slot) => !slot.varies).map((slot) => {
7744
7982
  const authored = api.props.find((pr) => pr.kind === "string" && pr.default === slot.default);
7745
- return authored === void 0 ? void 0 : { prop: authored.name, config: anchorSlug !== void 0 && slot.visibleIn.includes(anchorSlug) ? anchorSlug : slot.visibleIn[0] };
7983
+ if (authored === void 0) return void 0;
7984
+ if (slot.visibleIn.length > 0) {
7985
+ const entry = {
7986
+ prop: authored.name,
7987
+ config: anchorSlug !== void 0 && slot.visibleIn.includes(anchorSlug) ? anchorSlug : slot.visibleIn[0]
7988
+ };
7989
+ return entry;
7990
+ }
7991
+ if (slot.referencedIn.length > 0) {
7992
+ const toggle = api.props.find(
7993
+ (pr) => pr.kind === "boolean" && authored.name.toLowerCase().startsWith(pr.name.toLowerCase()) && authored.name.length > pr.name.length
7994
+ );
7995
+ if (toggle !== void 0 && anchorSlug !== void 0) {
7996
+ const entry = { prop: authored.name, config: anchorSlug, props: { [toggle.name]: true } };
7997
+ return entry;
7998
+ }
7999
+ }
8000
+ return void 0;
7746
8001
  }).filter((x) => x !== void 0);
7747
8002
  const { behaviors, prelude, disclosures } = authorBehaviors(api, { ...sentinels.length > 0 ? { sentinels } : {} });
7748
8003
  if (dismissName !== void 0) {
7749
- disclosures.push(`dismiss affordance detected from the recording (layer ${JSON.stringify(dismissName)}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
8004
+ disclosures.push(`dismiss affordance detected from the recording (${dismissName}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
7750
8005
  }
7751
8006
  for (const combo of api.syntheticCombos) {
7752
8007
  disclosures.push(`API split created a reachable pose with NO recorded truth: ${combo} (the design's exclusive axis cannot express it) \u2014 composed behavior only, disclosed to consumers`);
7753
8008
  }
7754
8009
  for (const slot of textSlots) {
7755
8010
  if (slot.varies) continue;
7756
- if (slot.visibleIn.length > 0) {
8011
+ const sentineled = sentinels.some((sn) => api.props.some((pr) => pr.name === sn.prop && pr.default === slot.default));
8012
+ if (sentineled) {
7757
8013
  disclosures.push(
7758
8014
  `constant content prop (recorded ${JSON.stringify(slot.default)}) is sentinel-checked: a behavior mounts it with a sentinel string and asserts it renders \u2014 hardcoding the recorded literal fails that check`
7759
8015
  );
8016
+ } else if (slot.referencedIn.length > 0) {
8017
+ disclosures.push(
8018
+ `content prop (recorded ${JSON.stringify(slot.default)}) is rendered by REFERENCE in the emission but has no visibility evidence and no boolean toggle to reveal it \u2014 sentinel skipped (it could fail honest components whose pose hides the node); wire the prop anyway`
8019
+ );
7760
8020
  } else {
7761
8021
  disclosures.push(
7762
8022
  `content prop from recorded text is UNEXERCISED AND UNRENDERED: NO recorded pose visibly renders ${JSON.stringify(slot.default)} (hidden node) \u2014 prescribed for completeness; wire it behind its visibility toggle; no sentinel can honestly run`
@@ -7781,7 +8041,7 @@ ALL prose instructions live ABOVE the task payload \u2014 the payload contains o
7781
8041
 
7782
8042
  ASSETS: inline the SVG assets you RENDER byte-verbatim, unchanged \u2014 never redraw or approximate an icon. Recorded assets that no scored config displays may be omitted. Two techniques reconcile that rule with reuse (both measured at 1.000): a glyph recorded once but shown in several colors keeps its bytes (fill attribute included) and is repainted with a CSS fill rule \u2014 a CSS declaration outranks an SVG presentation attribute, so one verbatim copy serves every tone; and multi-part glyphs needing fractional placement can nest each verbatim asset as a child <svg x= y=> inside one integer-origin frame (SVG user-space coordinates are exact), an alternative to the transform: scale() pattern.
7783
8043
 
7784
- GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height, floors not clamps); build to those dimensions, not to guessed viewports. Known rasterizer delta: Chrome often seats small text ONE PIXEL HIGHER than Figma in an identically sized box. Correct it with a PAINT-ONLY offset on those text runs \u2014 position: relative with top: 1px \u2014 never with padding or margin, which would grow the recorded box this same paragraph calls truth. TREAT IT AS A MEASUREMENT, NOT A RULE: measured cases are 12px/16px and 14px/20px needing the nudge and 14px/18px not, so font size alone does not predict it and neither does any formula we can currently defend. If small-text configs land just under the bar, apply the nudge, re-score, and keep it only if it helped. Four measured signatures, so do not expect one: on one kit it drove ink recall to 1.000; on another ink was already 1.000 and only similarity moved (mean 0.961 \u2192 0.976, four configs from 0.003 above the bar to 0.028); on a third it REGRESSED a passing bundle from 9/9 to 3/9 configs and had to be reverted; on a fourth it made every FAILING config worse too (tinted in-section surfaces: ink dropped ~0.011 across all eight configs and pushed a passing one under the bar). It is a hypothesis to score, never a default \u2014 NEVER apply it to a bundle that already passes, and when the failing family's diff shows a uniform low-density spread across the glyph band rather than a shifted band, the cause is rasterization weight, not seating: the nudge cannot help and a heavier hypothesis has no recorded truth to justify it \u2014 report the honest sub-bar result instead. Design-token NAMES in the payload are Figma names \u2014 canonicalize to valid CSS idents (lowercase kebab, e.g. "Text/text-primary" \u2192 --text-text-primary) if you emit tokens.css; literal values are equally acceptable. NEVER invent a token to satisfy a lint finding: quality findings are advisory, and a recorded value the kit has no token for is CORRECT as a literal \u2014 a made-up token name corrupts the tokens file as a record of the kit.
8044
+ GEOMETRY ARBITRATION (when emission styling and the recorded box disagree, the BOX wins): Figma strokes are INSIDE the box \u2014 border+padding sums that overshoot a recorded dimension mean use an inset box-shadow or subtract the border from the padding. That rule covers strokes AT the box edge only: when a config's reference PNG is LARGER than its recorded box, the recording itself proves an OUTWARD effect \u2014 a hover ring, glow, or shadow past the frame \u2014 and the outward part is drawn outward (box-shadow spread, outline), never forced inside. On such configs the score report carries a "seat" field ({x, y} \u2014 where the recorded box sits inside the larger reference), derived from the recording's own effect geometry (shadow offset/radius/spread) \u2014 informational, so an offset shadow's asymmetric bleed is not misread as a registration error. Following strokes-inside against a padded reference contradicts the recorded pad and cost a measured five configs a full round. The harness mounts each config at its recorded box (width \xD7 height, floors not clamps); build to those dimensions, not to guessed viewports. Known rasterizer delta: Chrome often seats small text ONE PIXEL HIGHER than Figma in an identically sized box. Correct it with a PAINT-ONLY offset on those text runs \u2014 position: relative with top: 1px \u2014 never with padding or margin, which would grow the recorded box this same paragraph calls truth. TREAT IT AS A MEASUREMENT, NOT A RULE: measured cases are 12px/16px and 14px/20px needing the nudge and 14px/18px not, so font size alone does not predict it and neither does any formula we can currently defend. RULE ORDER when both could apply (run 7: a config sat 0.0004 under the bar AND showed uniform spread \u2014 the rules pointed opposite ways): read the DIFF FIRST; the uniform-spread stop-rule below OUTRANKS the try-it rule here. Only when the diff shows a shifted band: apply the nudge, re-score, and keep it only if it helped. Four measured signatures, so do not expect one: on one kit it drove ink recall to 1.000; on another ink was already 1.000 and only similarity moved (mean 0.961 \u2192 0.976, four configs from 0.003 above the bar to 0.028); on a third it REGRESSED a passing bundle from 9/9 to 3/9 configs and had to be reverted; on a fourth it made every FAILING config worse too (tinted in-section surfaces: ink dropped ~0.011 across all eight configs and pushed a passing one under the bar). It is a hypothesis to score, never a default \u2014 NEVER apply it to a bundle that already passes, and when the failing family's diff shows a uniform low-density spread across the glyph band rather than a shifted band, the cause is rasterization weight, not seating: the nudge cannot help and a heavier hypothesis has no recorded truth to justify it \u2014 report the honest sub-bar result instead. Design-token NAMES in the payload are Figma names \u2014 canonicalize to valid CSS idents (lowercase kebab, e.g. "Text/text-primary" \u2192 --text-text-primary) if you emit tokens.css; literal values are equally acceptable. NEVER invent a token to satisfy a lint finding: quality findings are advisory, and a recorded value the kit has no token for is CORRECT as a literal \u2014 a made-up token name corrupts the tokens file as a record of the kit.
7785
8045
 
7786
8046
  RECORDED BOXES ARE TRUTH even when inconsistent: the same string may
7787
8047
  have different recorded widths across variants (designer resizing) \u2014 a
@@ -7869,10 +8129,10 @@ var init_brief = __esm({
7869
8129
  });
7870
8130
 
7871
8131
  // packages/generate/src/segments.ts
7872
- import { existsSync as existsSync21, readFileSync as readFileSync18, readdirSync as readdirSync5 } from "node:fs";
7873
- import path27 from "node:path";
8132
+ import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
8133
+ import path28 from "node:path";
7874
8134
  function repText(set, rep, tool) {
7875
- const env = JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8"));
8135
+ const env = JSON.parse(readFileSync19(path28.join(set, rep, `${tool}.json`), "utf8"));
7876
8136
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
7877
8137
  }
7878
8138
  function stripFigmaInstructions(emission) {
@@ -7933,17 +8193,17 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
7933
8193
  function buildSegments(task, mode = "fenced") {
7934
8194
  const SET = task.set;
7935
8195
  let rawDefs = {};
7936
- if (existsSync21(path27.join(SET, "get_variable_defs.json"))) {
7937
- const text = envelopeFirstTextPart(JSON.parse(readFileSync18(path27.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
8196
+ if (existsSync22(path28.join(SET, "get_variable_defs.json"))) {
8197
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path28.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
7938
8198
  try {
7939
8199
  rawDefs = JSON.parse(text);
7940
8200
  } catch {
7941
8201
  }
7942
8202
  } else {
7943
8203
  for (const cfg of task.configs) {
7944
- const f = path27.join(SET, cfg.rep, "get_variable_defs.json");
7945
- if (!existsSync21(f)) continue;
7946
- const text = envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) || "{}";
8204
+ const f = path28.join(SET, cfg.rep, "get_variable_defs.json");
8205
+ if (!existsSync22(f)) continue;
8206
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
7947
8207
  try {
7948
8208
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
7949
8209
  } catch {
@@ -7951,8 +8211,8 @@ function buildSegments(task, mode = "fenced") {
7951
8211
  }
7952
8212
  }
7953
8213
  const emissionTexts = task.configs.map((cfg) => {
7954
- const f = path27.join(SET, cfg.rep, "get_design_context.json");
7955
- return existsSync21(f) ? envelopeFirstTextPart(JSON.parse(readFileSync18(f, "utf8"))) : "";
8214
+ const f = path28.join(SET, cfg.rep, "get_design_context.json");
8215
+ return existsSync22(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
7956
8216
  });
7957
8217
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
7958
8218
  const defs = JSON.stringify(map, null, 1);
@@ -7967,9 +8227,9 @@ ${defs}
7967
8227
  for (const cfg of task.configs) {
7968
8228
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
7969
8229
  const emission = stripFigmaInstructions(repText(SET, cfg.rep, "get_design_context"));
7970
- const assets = readdirSync5(path27.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
8230
+ const assets = readdirSync6(path28.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
7971
8231
  \`\`\`svg
7972
- ${readFileSync18(path27.join(SET, cfg.rep, f), "utf8")}
8232
+ ${readFileSync19(path28.join(SET, cfg.rep, f), "utf8")}
7973
8233
  \`\`\``).join("\n");
7974
8234
  parts.push(`
7975
8235
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -8062,8 +8322,8 @@ var init_adapter = __esm({
8062
8322
 
8063
8323
  // packages/generate/src/bundle-emit.ts
8064
8324
  import { createHash as createHash3 } from "node:crypto";
8065
- import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6, writeFileSync as writeFileSync10 } from "node:fs";
8066
- import path28 from "node:path";
8325
+ import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
8326
+ import path29 from "node:path";
8067
8327
  function pinFromConfigs(configs) {
8068
8328
  const domains = /* @__PURE__ */ new Map();
8069
8329
  const kinds = /* @__PURE__ */ new Map();
@@ -8112,22 +8372,22 @@ function cssFontFamilies(css) {
8112
8372
  return [...out];
8113
8373
  }
8114
8374
  function countLatticeSymbols(setDir) {
8115
- const manifestFile = path28.join(setDir, "recording-set.json");
8116
- if (existsSync22(manifestFile)) {
8375
+ const manifestFile = path29.join(setDir, "recording-set.json");
8376
+ if (existsSync23(manifestFile)) {
8117
8377
  try {
8118
- const lattice = JSON.parse(readFileSync19(manifestFile, "utf8")).latticeNames;
8378
+ const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
8119
8379
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
8120
8380
  } catch {
8121
8381
  }
8122
8382
  }
8123
8383
  const files = [
8124
- path28.join(setDir, "get_metadata.json"),
8125
- ...existsSync22(setDir) ? readdirSync6(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path28.join(setDir, f)) : []
8126
- ].filter((f) => existsSync22(f));
8384
+ path29.join(setDir, "get_metadata.json"),
8385
+ ...existsSync23(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path29.join(setDir, f)) : []
8386
+ ].filter((f) => existsSync23(f));
8127
8387
  if (files.length === 0) return null;
8128
8388
  let count = 0;
8129
8389
  for (const f of files) {
8130
- const text = envelopeTextContent(JSON.parse(readFileSync19(f, "utf8")));
8390
+ const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
8131
8391
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
8132
8392
  }
8133
8393
  return count > 0 ? count : null;
@@ -8135,21 +8395,21 @@ function countLatticeSymbols(setDir) {
8135
8395
  function recordingSetHash(setDir, configs) {
8136
8396
  const relPaths = [];
8137
8397
  for (const name of ["recording-set.json", "completeness-manifest.json", "get_variable_defs.json", "get_metadata.json"]) {
8138
- if (existsSync22(path28.join(setDir, name))) relPaths.push(name);
8398
+ if (existsSync23(path29.join(setDir, name))) relPaths.push(name);
8139
8399
  }
8140
8400
  for (const cfg of configs) {
8141
8401
  for (const f of ["get_design_context.json", "get_metadata.json", "get_screenshot.json", "get_variable_defs.json"]) {
8142
- if (existsSync22(path28.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
8402
+ if (existsSync23(path29.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
8143
8403
  }
8144
- if (existsSync22(path28.join(setDir, cfg.rep))) {
8145
- for (const asset of readdirSync6(path28.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
8404
+ if (existsSync23(path29.join(setDir, cfg.rep))) {
8405
+ for (const asset of readdirSync7(path29.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
8146
8406
  relPaths.push(`${cfg.rep}/${asset}`);
8147
8407
  }
8148
8408
  }
8149
8409
  }
8150
8410
  return hashRecordingSet(
8151
8411
  relPaths,
8152
- (p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
8412
+ (p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
8153
8413
  (chunks) => {
8154
8414
  const h = createHash3("sha256");
8155
8415
  for (const c of chunks) h.update(c);
@@ -8161,14 +8421,14 @@ function statusOf(s) {
8161
8421
  return tierOf(s, BARS.cert);
8162
8422
  }
8163
8423
  function emitBundleV1(opts) {
8164
- const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, status: statusOf(s) }));
8424
+ const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: statusOf(s) }));
8165
8425
  const certified = statuses.filter((s) => s.status === "certified").length;
8166
8426
  const pass = statuses.filter((s) => s.status !== "fail").length;
8167
8427
  const lattice = countLatticeSymbols(opts.task.set);
8168
8428
  const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
8169
8429
  const prelude = opts.behaviors.filter((b) => b.id.startsWith("prelude:"));
8170
- const cssFiles = ["styles.css", "tokens.css"].map((f) => path28.join(opts.bundleDir, f)).filter((f) => existsSync22(f));
8171
- const families = cssFontFamilies(cssFiles.map((f) => readFileSync19(f, "utf8")).join("\n"));
8430
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path29.join(opts.bundleDir, f)).filter((f) => existsSync23(f));
8431
+ const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
8172
8432
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
8173
8433
  family: f.family,
8174
8434
  weight: f.weight,
@@ -8197,7 +8457,7 @@ function emitBundleV1(opts) {
8197
8457
  // resolvable via verify's --set override).
8198
8458
  path: (() => {
8199
8459
  const base = process.env["INIT_CWD"] ?? process.cwd();
8200
- const rel = path28.relative(base, opts.task.set);
8460
+ const rel = path29.relative(base, opts.task.set);
8201
8461
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
8202
8462
  })(),
8203
8463
  component: opts.componentName,
@@ -8221,16 +8481,16 @@ function emitBundleV1(opts) {
8221
8481
  })
8222
8482
  };
8223
8483
  const written = [];
8224
- const manifestPath2 = path28.join(opts.bundleDir, "component.json");
8225
- writeFileSync10(manifestPath2, `${JSON.stringify(manifest, null, 2)}
8484
+ const manifestPath2 = path29.join(opts.bundleDir, "component.json");
8485
+ writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
8226
8486
  `);
8227
8487
  written.push(manifestPath2);
8228
- const stylesPath = path28.join(opts.bundleDir, "styles.css");
8229
- if (existsSync22(stylesPath)) {
8488
+ const stylesPath = path29.join(opts.bundleDir, "styles.css");
8489
+ if (existsSync23(stylesPath)) {
8230
8490
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
8231
- const current = readFileSync19(stylesPath, "utf8");
8491
+ const current = readFileSync20(stylesPath, "utf8");
8232
8492
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
8233
- writeFileSync10(stylesPath, `${comment}
8493
+ writeFileSync11(stylesPath, `${comment}
8234
8494
  ${stripped}`);
8235
8495
  written.push(stylesPath);
8236
8496
  }
@@ -8278,8 +8538,8 @@ __export(fonts_exports, {
8278
8538
  runFontsResolveSet: () => runFontsResolveSet,
8279
8539
  runFontsStatus: () => runFontsStatus
8280
8540
  });
8281
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
8282
- import path29 from "node:path";
8541
+ import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
8542
+ import path30 from "node:path";
8283
8543
  async function runFontsResolve(opts) {
8284
8544
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
8285
8545
  emitData(opts, result, () => {
@@ -8294,7 +8554,7 @@ async function runFontsResolve(opts) {
8294
8554
  }
8295
8555
  }
8296
8556
  async function runFontsResolveSet(opts) {
8297
- const setDir = path29.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
8557
+ const setDir = path30.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
8298
8558
  let needs = [];
8299
8559
  try {
8300
8560
  needs = recordedFontNeeds(setDir);
@@ -8343,16 +8603,16 @@ async function runFontsResolveSet(opts) {
8343
8603
  }
8344
8604
  }
8345
8605
  function runFontsStatus(opts) {
8346
- const manifestPath2 = path29.join(opts.cacheDir, "manifest.json");
8347
- if (!existsSync23(manifestPath2)) {
8606
+ const manifestPath2 = path30.join(opts.cacheDir, "manifest.json");
8607
+ if (!existsSync24(manifestPath2)) {
8348
8608
  fail(opts, ExitCode.FontsUnproven, {
8349
8609
  error: `no font cache at ${opts.cacheDir}`,
8350
8610
  code: "fonts-unresolved",
8351
8611
  remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
8352
8612
  });
8353
8613
  }
8354
- const faces = JSON.parse(readFileSync20(manifestPath2, "utf8"));
8355
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path29.resolve(opts.lock), opts.cacheDir) : null;
8614
+ const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
8615
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path30.resolve(opts.lock), opts.cacheDir) : null;
8356
8616
  emitData(opts, { faces, lockVerdicts }, () => {
8357
8617
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
8358
8618
  `);
@@ -8437,8 +8697,8 @@ __export(verify_exports, {
8437
8697
  interactionCoverage: () => interactionCoverage,
8438
8698
  runVerify: () => runVerify
8439
8699
  });
8440
- import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
8441
- import path30 from "node:path";
8700
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
8701
+ import path31 from "node:path";
8442
8702
  function interactionCoverage(behaviors) {
8443
8703
  const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
8444
8704
  return {
@@ -8453,7 +8713,7 @@ function taskFromManifest(opts, manifest, setDir) {
8453
8713
  const adapterSlugs = Object.keys(manifest.propAdapter);
8454
8714
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
8455
8715
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
8456
- const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
8716
+ const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
8457
8717
  const authored = (() => {
8458
8718
  if (registry !== void 0) return void 0;
8459
8719
  try {
@@ -8485,19 +8745,19 @@ function taskFromManifest(opts, manifest, setDir) {
8485
8745
  }
8486
8746
  async function runVerify(opts) {
8487
8747
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8488
- const setOverride = opts.set !== void 0 ? path30.resolve(callerCwd, opts.set) : void 0;
8489
- opts = { ...opts, bundleDir: path30.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
8490
- if (!existsSync24(opts.bundleDir)) {
8748
+ const setOverride = opts.set !== void 0 ? path31.resolve(callerCwd, opts.set) : void 0;
8749
+ opts = { ...opts, bundleDir: path31.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
8750
+ if (!existsSync25(opts.bundleDir)) {
8491
8751
  fail(opts, ExitCode.InputValidation, {
8492
8752
  error: `bundle directory not found: ${opts.bundleDir}`,
8493
8753
  code: "bundle-missing",
8494
8754
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
8495
8755
  });
8496
8756
  }
8497
- const manifestPath2 = path30.join(opts.bundleDir, "component.json");
8757
+ const manifestPath2 = path31.join(opts.bundleDir, "component.json");
8498
8758
  let manifest;
8499
- if (existsSync24(manifestPath2)) {
8500
- const { manifest: parsed, issues } = readBundleManifest(readFileSync21(manifestPath2, "utf8"));
8759
+ if (existsSync25(manifestPath2)) {
8760
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
8501
8761
  if (issues.length > 0) {
8502
8762
  fail(opts, ExitCode.InputValidation, {
8503
8763
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -8524,21 +8784,21 @@ async function runVerify(opts) {
8524
8784
  task = registry;
8525
8785
  } else if (manifest !== void 0) {
8526
8786
  const resolveSetDir = (p) => {
8527
- if (path30.isAbsolute(p)) return p;
8528
- const fromRepo = path30.resolve(REPO_ROOT, p);
8529
- if (existsSync24(fromRepo)) return fromRepo;
8530
- return path30.resolve(callerCwd, p);
8787
+ if (path31.isAbsolute(p)) return p;
8788
+ const fromRepo = path31.resolve(REPO_ROOT, p);
8789
+ if (existsSync25(fromRepo)) return fromRepo;
8790
+ return path31.resolve(callerCwd, p);
8531
8791
  };
8532
8792
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
8533
- if (!existsSync24(path30.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path30.resolve(t.set) === path30.resolve(setDir))) {
8793
+ if (!existsSync25(path31.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path31.resolve(t.set) === path31.resolve(setDir))) {
8534
8794
  fail(opts, ExitCode.RecordingIncomplete, {
8535
8795
  error: `recording set not found or unmanifested: ${setDir}`,
8536
8796
  code: "recording-set-missing",
8537
8797
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
8538
8798
  });
8539
8799
  }
8540
- const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
8541
- if (registry !== void 0 && !existsSync24(path30.join(setDir, "recording-set.json"))) {
8800
+ const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
8801
+ if (registry !== void 0 && !existsSync25(path31.join(setDir, "recording-set.json"))) {
8542
8802
  const recordedSlugs = registry.configs.map((c) => c.rep);
8543
8803
  const adapterSlugs = Object.keys(manifest.propAdapter);
8544
8804
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -8562,9 +8822,9 @@ async function runVerify(opts) {
8562
8822
  warn(opts, `recording set content differs from the bundle's provenance stamp (${hash.slice(0, 12)}\u2026 vs ${manifest.provenance.recordingSet.hash.slice(0, 12)}\u2026) \u2014 scores apply to the CURRENT set`);
8563
8823
  }
8564
8824
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
8565
- const p = path30.join(opts.bundleDir, name);
8566
- if (!existsSync24(p)) continue;
8567
- const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync21(p)));
8825
+ const p = path31.join(opts.bundleDir, name);
8826
+ if (!existsSync25(p)) continue;
8827
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
8568
8828
  if (issues.length > 0) {
8569
8829
  fail(opts, ExitCode.InputValidation, {
8570
8830
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -8588,7 +8848,7 @@ async function runVerify(opts) {
8588
8848
  });
8589
8849
  }
8590
8850
  const missing = task.configs.filter(
8591
- (c) => !existsSync24(path30.join(task.set, c.rep, "get_screenshot.json")) || !existsSync24(path30.join(task.set, c.rep, "get_metadata.json"))
8851
+ (c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
8592
8852
  );
8593
8853
  if (missing.length > 0) {
8594
8854
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -8598,10 +8858,10 @@ async function runVerify(opts) {
8598
8858
  });
8599
8859
  }
8600
8860
  const bar = BARS2[opts.bar];
8601
- const evidenceDir = path30.join(opts.bundleDir, "verify-evidence");
8861
+ const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
8602
8862
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
8603
8863
  const quality = await checkBundleQuality(opts.bundleDir, task.entry);
8604
- const bundleCss = ["tokens.css", "styles.css"].map((f) => path30.join(opts.bundleDir, f)).filter((f) => existsSync24(f)).map((f) => readFileSync21(f, "utf8")).join("\n");
8864
+ const bundleCss = ["tokens.css", "styles.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n");
8605
8865
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
8606
8866
  const parity = await checkHoverParity(task, opts.bundleDir);
8607
8867
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
@@ -8818,18 +9078,18 @@ __export(engine_exports, {
8818
9078
  runEngineBrief: () => runEngineBrief,
8819
9079
  runEngineScore: () => runEngineScore
8820
9080
  });
8821
- import { existsSync as existsSync25, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
8822
- import path31 from "node:path";
9081
+ import { existsSync as existsSync26, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
9082
+ import path32 from "node:path";
8823
9083
  function resolveEngineTask(opts, callerCwd) {
8824
- const asPath = path31.resolve(callerCwd, opts.taskOrSet);
8825
- const isSet = existsSync25(path31.join(asPath, "recording-set.json"));
9084
+ const asPath = path32.resolve(callerCwd, opts.taskOrSet);
9085
+ const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
8826
9086
  const registry = TASKS[opts.taskOrSet];
8827
9087
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
8828
9088
  if (isSet) {
8829
9089
  try {
8830
9090
  const authored = authorTaskFromSet(asPath);
8831
9091
  for (const d of authored.disclosures) warn(opts, d);
8832
- return { task: authored.task, name: path31.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
9092
+ return { task: authored.task, name: path32.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
8833
9093
  } catch (err) {
8834
9094
  fail(opts, ExitCode.InputValidation, {
8835
9095
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -8845,15 +9105,16 @@ function resolveEngineTask(opts, callerCwd) {
8845
9105
  });
8846
9106
  }
8847
9107
  function runEngineBrief(opts) {
9108
+ requireEntitlement(opts);
8848
9109
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8849
9110
  const { task, name } = resolveEngineTask(opts, callerCwd);
8850
9111
  const bar = BARS3[opts.bar];
8851
9112
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" });
8852
9113
  const segments = buildSegments(task, "files");
8853
9114
  let notRecorded;
8854
- const manifestPath2 = path31.join(task.set, "recording-set.json");
8855
- if (existsSync25(manifestPath2)) {
8856
- notRecorded = JSON.parse(readFileSync22(manifestPath2, "utf8")).notRecorded;
9115
+ const manifestPath2 = path32.join(task.set, "recording-set.json");
9116
+ if (existsSync26(manifestPath2)) {
9117
+ notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
8857
9118
  }
8858
9119
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
8859
9120
 
@@ -8861,7 +9122,7 @@ function runEngineBrief(opts) {
8861
9122
  DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of recorded per-axis truth (the custom-property composition rule: one paint axis sets variables, the other consumes) and list every such pose in your report as UNRECORDED-COMPOSED. Recording them is the real fix: re-plan the set (full matrix is the default) and record the missing poses.
8862
9123
  ${notRecorded}` : "";
8863
9124
  let fontProvisioning;
8864
- if (existsSync25(manifestPath2)) {
9125
+ if (existsSync26(manifestPath2)) {
8865
9126
  const provided = resolvedFontFamilies().map((f) => f.family);
8866
9127
  const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
8867
9128
  if (unprovided.length > 0) {
@@ -8883,9 +9144,9 @@ ${notRecorded}` : "";
8883
9144
 
8884
9145
  === TASK PAYLOAD (recorded truth, verbatim) ===
8885
9146
  ${segments}`;
8886
- const payloadFile = path31.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
8887
- mkdirSync6(path31.dirname(payloadFile), { recursive: true });
8888
- writeFileSync11(payloadFile, payload);
9147
+ const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
9148
+ mkdirSync7(path32.dirname(payloadFile), { recursive: true });
9149
+ writeFileSync12(payloadFile, payload);
8889
9150
  emitData(
8890
9151
  opts,
8891
9152
  {
@@ -8927,10 +9188,11 @@ ${segments}`;
8927
9188
  );
8928
9189
  }
8929
9190
  async function runEngineScore(opts) {
9191
+ requireEntitlement(opts);
8930
9192
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8931
- const candidateDir = path31.resolve(callerCwd, opts.candidateDir);
9193
+ const candidateDir = path32.resolve(callerCwd, opts.candidateDir);
8932
9194
  const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
8933
- if (!existsSync25(candidateDir)) {
9195
+ if (!existsSync26(candidateDir)) {
8934
9196
  fail(opts, ExitCode.InputValidation, {
8935
9197
  error: `candidate directory not found: ${candidateDir}`,
8936
9198
  code: "candidate-missing",
@@ -8944,10 +9206,10 @@ async function runEngineScore(opts) {
8944
9206
  remediation: fontsUnprovenRemediation(task.set)
8945
9207
  });
8946
9208
  }
8947
- if (opts.rebind !== true && existsSync25(path31.join(candidateDir, "component.json"))) {
9209
+ if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
8948
9210
  const prior = (() => {
8949
9211
  try {
8950
- const read = readBundleManifest(readFileSync22(path31.join(candidateDir, "component.json"), "utf8"));
9212
+ const read = readBundleManifest(readFileSync23(path32.join(candidateDir, "component.json"), "utf8"));
8951
9213
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
8952
9214
  } catch {
8953
9215
  return { unreadable: true };
@@ -8969,7 +9231,7 @@ async function runEngineScore(opts) {
8969
9231
  }
8970
9232
  }
8971
9233
  const bar = BARS3[opts.bar];
8972
- const evidenceDir = path31.join(candidateDir, "verify-evidence");
9234
+ const evidenceDir = path32.join(candidateDir, "verify-evidence");
8973
9235
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
8974
9236
  const parity = await checkHoverParity(task, candidateDir);
8975
9237
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
@@ -8984,11 +9246,16 @@ ${[
8984
9246
  ...quality.findings.map((f) => `- ${f.kind} ${f.file}${f.line === void 0 ? "" : `:${f.line}`} \u2014 ${f.message}`),
8985
9247
  ...quality.tokensAbsent ? ["- no design tokens: no tokens.css and no var(--\u2026) reference; every value is hardcoded"] : []
8986
9248
  ].join("\n")}`;
8987
- const feedback = buildFeedback(scores, behaviors, bar, "files") + qualityFeedback;
8988
9249
  const allPass = obj[0] === total && total > 0;
8989
9250
  const certBar = BARS3["cert"];
8990
- const certifiedReps = scores.filter((sc) => sc.similarity >= certBar.sim && sc.inkRecall >= certBar.ink).map((sc) => sc.rep);
9251
+ const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
9252
+ const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep)).map((sc) => sc.rep);
8991
9253
  const certifiedSet = new Set(certifiedReps);
9254
+ const certificationFeedback = `
9255
+
9256
+ CERTIFICATION: ${certifiedReps.length}/${scores.length} configs at the certification bar (sim \u2265${certBar.sim} AND ink \u2265${certBar.ink}, exact values, after parity demotion \u2014 composition checks at verify can demote further).${certifiedReps.length < scores.length ? ` Below cert: ${scores.filter((sc) => !certifiedSet.has(sc.rep)).map((sc) => sc.rep).join(", ")}.` : ""}
9257
+ METRIC DEADBAND (read before iterating on near-misses): the scored similarity/ink deliberately tolerate \xB11px edge shift and antialiased-edge differences \u2014 cross-rasterizer noise absorption. A change entirely inside that band moves these numbers by EXACTLY ZERO (working as designed, not a stuck scorer). The per-config \`exact\` fields in the JSON are tolerance-free and move first: compare exact across rounds to confirm a small fix landed, and stop iterating when only exact moves \u2014 the bar reads the tolerant numbers.`;
9258
+ const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + qualityFeedback;
8992
9259
  const emitted = emitBundleV1({
8993
9260
  bundleDir: candidateDir,
8994
9261
  task,
@@ -9019,7 +9286,7 @@ ${[
9019
9286
  bundleManifest: emitted.written[0],
9020
9287
  note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
9021
9288
  allPass,
9022
- certification: { certified: certifiedReps.length, total, bar: certBar }
9289
+ certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity demotion; verify's composition checks can demote further" }
9023
9290
  },
9024
9291
  () => {
9025
9292
  for (const s of scores) process.stdout.write(`${s.pass ? certifiedSet.has(s.rep) ? "CERT" : "PASS" : "FAIL"} ${s.rep}: sim=${s.similarity} ink=${s.inkRecall}${s.error !== void 0 ? ` [${s.error}]` : ""}
@@ -9049,7 +9316,9 @@ var init_engine2 = __esm({
9049
9316
  init_font_guidance();
9050
9317
  init_src4();
9051
9318
  init_output();
9319
+ init_entitlement();
9052
9320
  init_verify();
9321
+ init_src4();
9053
9322
  BARS3 = {
9054
9323
  pass: { sim: 0.95, ink: 0.95 },
9055
9324
  cert: { sim: 0.97, ink: 0.95 }
@@ -9062,11 +9331,11 @@ var codeconnect_exports = {};
9062
9331
  __export(codeconnect_exports, {
9063
9332
  runCodeConnect: () => runCodeConnect
9064
9333
  });
9065
- import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
9066
- import path32 from "node:path";
9334
+ import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
9335
+ import path33 from "node:path";
9067
9336
  function runCodeConnect(opts) {
9068
9337
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9069
- const bundleDir = path32.resolve(callerCwd, opts.bundleDir);
9338
+ const bundleDir = path33.resolve(callerCwd, opts.bundleDir);
9070
9339
  let url;
9071
9340
  try {
9072
9341
  url = new URL(opts.figmaUrl);
@@ -9082,7 +9351,7 @@ function runCodeConnect(opts) {
9082
9351
  }
9083
9352
  let manifest;
9084
9353
  try {
9085
- const read = readBundleManifest(readFileSync23(path32.join(bundleDir, "component.json"), "utf8"));
9354
+ const read = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8"));
9086
9355
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
9087
9356
  manifest = read.manifest;
9088
9357
  } catch (err) {
@@ -9092,8 +9361,8 @@ function runCodeConnect(opts) {
9092
9361
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
9093
9362
  });
9094
9363
  }
9095
- const setDir = path32.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
9096
- if (!existsSync26(path32.join(setDir, "recording-set.json"))) {
9364
+ const setDir = path33.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
9365
+ if (!existsSync27(path33.join(setDir, "recording-set.json"))) {
9097
9366
  fail(opts, ExitCode.InputValidation, {
9098
9367
  error: `recording set not found at ${setDir}`,
9099
9368
  code: "codeconnect-no-set",
@@ -9114,10 +9383,10 @@ function runCodeConnect(opts) {
9114
9383
  const component = api.component;
9115
9384
  const recManifest = loadManifest(setDir);
9116
9385
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
9117
- const meta = path32.join(setDir, r.slug, "get_metadata.json");
9118
- if (!existsSync26(meta)) return void 0;
9386
+ const meta = path33.join(setDir, r.slug, "get_metadata.json");
9387
+ if (!existsSync27(meta)) return void 0;
9119
9388
  try {
9120
- return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync23(meta, "utf8"))))?.[1];
9389
+ return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
9121
9390
  } catch {
9122
9391
  return void 0;
9123
9392
  }
@@ -9175,7 +9444,7 @@ function runCodeConnect(opts) {
9175
9444
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
9176
9445
  fragmentVars.push(varName);
9177
9446
  }
9178
- const entryRel = path32.relative(callerCwd, path32.join(bundleDir, manifest.entry));
9447
+ const entryRel = path33.relative(callerCwd, path33.join(bundleDir, manifest.entry));
9179
9448
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
9180
9449
  const lines = [
9181
9450
  `// url=${opts.figmaUrl}`,
@@ -9196,8 +9465,8 @@ function runCodeConnect(opts) {
9196
9465
  `}`,
9197
9466
  ``
9198
9467
  ].join("\n");
9199
- const outFile = path32.resolve(callerCwd, opts.out ?? path32.join(bundleDir, `${component}.figma.ts`));
9200
- writeFileSync12(outFile, lines);
9468
+ const outFile = path33.resolve(callerCwd, opts.out ?? path33.join(bundleDir, `${component}.figma.ts`));
9469
+ writeFileSync13(outFile, lines);
9201
9470
  emitData(
9202
9471
  opts,
9203
9472
  {
@@ -9257,17 +9526,17 @@ __export(generate_recorded_exports, {
9257
9526
  runGenerateRecorded: () => runGenerateRecorded
9258
9527
  });
9259
9528
  import { confirm as confirm3, isCancel as isCancel3 } from "@clack/prompts";
9260
- import { existsSync as existsSync27, readFileSync as readFileSync24 } from "node:fs";
9261
- import path33 from "node:path";
9529
+ import { existsSync as existsSync28, readFileSync as readFileSync25 } from "node:fs";
9530
+ import path34 from "node:path";
9262
9531
  async function runGenerateRecorded(opts) {
9263
9532
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9264
- const outDirAbs = path33.resolve(callerCwd, opts.out);
9265
- const recordedAsPath = path33.resolve(callerCwd, opts.recorded);
9533
+ const outDirAbs = path34.resolve(callerCwd, opts.out);
9534
+ const recordedAsPath = path34.resolve(callerCwd, opts.recorded);
9266
9535
  let task;
9267
9536
  let taskName;
9268
9537
  let authoredApi;
9269
9538
  let composition;
9270
- const isSet = existsSync27(path33.join(recordedAsPath, "recording-set.json"));
9539
+ const isSet = existsSync28(path34.join(recordedAsPath, "recording-set.json"));
9271
9540
  const registry = TASKS[opts.recorded];
9272
9541
  if (registry !== void 0 && !isSet) {
9273
9542
  task = registry;
@@ -9276,7 +9545,7 @@ async function runGenerateRecorded(opts) {
9276
9545
  try {
9277
9546
  const authored = authorTaskFromSet(recordedAsPath);
9278
9547
  task = authored.task;
9279
- taskName = path33.basename(recordedAsPath);
9548
+ taskName = path34.basename(recordedAsPath);
9280
9549
  authoredApi = authored.api;
9281
9550
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
9282
9551
  if (roles.success) composition = roles.data;
@@ -9303,7 +9572,7 @@ async function runGenerateRecorded(opts) {
9303
9572
  });
9304
9573
  }
9305
9574
  const missing = task.configs.filter(
9306
- (c) => !existsSync27(path33.join(task.set, c.rep, "get_screenshot.json")) || !existsSync27(path33.join(task.set, c.rep, "get_metadata.json")) || !existsSync27(path33.join(task.set, c.rep, "get_design_context.json"))
9575
+ (c) => !existsSync28(path34.join(task.set, c.rep, "get_screenshot.json")) || !existsSync28(path34.join(task.set, c.rep, "get_metadata.json")) || !existsSync28(path34.join(task.set, c.rep, "get_design_context.json"))
9307
9576
  );
9308
9577
  if (missing.length > 0) {
9309
9578
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -9373,8 +9642,8 @@ async function runGenerateRecorded(opts) {
9373
9642
  ` : `${line}
9374
9643
  `);
9375
9644
  if (opts.dryRun) {
9376
- emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path33.join(outDirAbs, taskName) }, () => {
9377
- process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path33.join(outDirAbs, taskName)})
9645
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path34.join(outDirAbs, taskName) }, () => {
9646
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path34.join(outDirAbs, taskName)})
9378
9647
  `);
9379
9648
  });
9380
9649
  return;
@@ -9397,10 +9666,10 @@ async function runGenerateRecorded(opts) {
9397
9666
  });
9398
9667
  }
9399
9668
  }
9400
- const bundleDir = path33.join(outDirAbs, taskName);
9401
- if (existsSync27(path33.join(bundleDir, "component.json"))) {
9669
+ const bundleDir = path34.join(outDirAbs, taskName);
9670
+ if (existsSync28(path34.join(bundleDir, "component.json"))) {
9402
9671
  try {
9403
- const prior = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8")).manifest;
9672
+ const prior = readBundleManifest(readFileSync25(path34.join(bundleDir, "component.json"), "utf8")).manifest;
9404
9673
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
9405
9674
  fail(opts, ExitCode.InputValidation, {
9406
9675
  error: `${bundleDir} already holds a bundle bound to recording set "${prior.provenance.recordingSet.path}" \u2014 generating here against a different set would silently rewrite its verification identity`,
@@ -9536,8 +9805,9 @@ import { Command } from "commander";
9536
9805
  // packages/cli/src/commands/doctor.ts
9537
9806
  init_src4();
9538
9807
  init_src();
9539
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
9540
- import path21 from "node:path";
9808
+ import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
9809
+ import os4 from "node:os";
9810
+ import path22 from "node:path";
9541
9811
 
9542
9812
  // packages/cli/src/describe.ts
9543
9813
  var COMMON_EXIT_CODES = {
@@ -9556,6 +9826,7 @@ function printDescription(description) {
9556
9826
  init_env();
9557
9827
  init_environment();
9558
9828
  init_output();
9829
+ init_entitlement();
9559
9830
  var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
9560
9831
  var DOCTOR_DESCRIPTION = {
9561
9832
  name: "doctor",
@@ -9615,15 +9886,38 @@ async function runDoctorChecks(options) {
9615
9886
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
9616
9887
  });
9617
9888
  }
9618
- const fontManifest = path21.join(fontCacheDir(), "manifest.json");
9889
+ const fontManifest = path22.join(fontCacheDir(), "manifest.json");
9619
9890
  checks.push(
9620
- existsSync16(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync13(fontManifest, "utf8")).length} faces)` } : {
9891
+ existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
9621
9892
  name: "font-cache",
9622
9893
  ok: true,
9623
9894
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
9624
9895
  remediation: "Nothing to do now: `tendril fonts resolve --set <recording-dir>` fetches exactly what a recording declares, and generate/verify name that command when they need it."
9625
9896
  }
9626
9897
  );
9898
+ const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
9899
+ if (existsSync17(pluginRoot)) {
9900
+ try {
9901
+ const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
9902
+ const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
9903
+ if (newest !== void 0) {
9904
+ const skewed = versionIsNewer(newest, cliVersion());
9905
+ checks.push(
9906
+ skewed ? {
9907
+ name: "plugin-skew",
9908
+ ok: false,
9909
+ detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
9910
+ remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
9911
+ } : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
9912
+ );
9913
+ }
9914
+ } catch {
9915
+ }
9916
+ }
9917
+ const ent = checkEntitlement();
9918
+ checks.push(
9919
+ ent.ok ? ent.mode === "pre-launch" ? { name: "entitlement", ok: true, detail: "pre-launch build \u2014 entitlement gates present but unarmed (no public key shipped); record/generate run freely" } : { name: "entitlement", ok: true, detail: `active plan "${ent.claims.plan}" until ${new Date(ent.claims.exp).toISOString().slice(0, 10)}${ent.stale ? " (stale \u2014 renews at next opportunity)" : ""}` } : { name: "entitlement", ok: false, detail: ent.error, remediation: ent.remediation }
9920
+ );
9627
9921
  const figmaToken = resolveCredential("FIGMA_TOKEN");
9628
9922
  checks.push({
9629
9923
  name: "figma-pat",
@@ -9692,7 +9986,7 @@ async function runDoctor(flags) {
9692
9986
  init_src3();
9693
9987
  import { intro, isCancel, outro, password } from "@clack/prompts";
9694
9988
  import fs from "node:fs";
9695
- import path22 from "node:path";
9989
+ import path23 from "node:path";
9696
9990
  init_env();
9697
9991
  init_output();
9698
9992
  var INIT_DESCRIPTION = {
@@ -9731,7 +10025,7 @@ async function runInit(flags) {
9731
10025
  printDescription(INIT_DESCRIPTION);
9732
10026
  return;
9733
10027
  }
9734
- const envPath = path22.resolve(process.cwd(), ".env");
10028
+ const envPath = path23.resolve(process.cwd(), ".env");
9735
10029
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
9736
10030
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
9737
10031
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -9752,7 +10046,7 @@ async function runInit(flags) {
9752
10046
  next.set(ENV_KEYS.figma, figmaToken);
9753
10047
  next.set(ENV_KEYS.openrouter, openrouterKey);
9754
10048
  const changed = existing.get(ENV_KEYS.figma) !== next.get(ENV_KEYS.figma) || existing.get(ENV_KEYS.openrouter) !== next.get(ENV_KEYS.openrouter);
9755
- const gitignorePath = path22.resolve(process.cwd(), ".gitignore");
10049
+ const gitignorePath = path23.resolve(process.cwd(), ".gitignore");
9756
10050
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
9757
10051
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
9758
10052
  if (flags.dryRun) {
@@ -9803,16 +10097,17 @@ init_src();
9803
10097
  init_src5();
9804
10098
  init_src2();
9805
10099
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
9806
- import { readFileSync as readFileSync14, readdirSync as readdirSync3, existsSync as existsSync17 } from "node:fs";
10100
+ import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
9807
10101
  init_env();
9808
10102
  init_output();
10103
+ init_entitlement();
9809
10104
 
9810
10105
  // packages/cli/src/pipeline.ts
9811
10106
  init_src2();
9812
10107
  init_src4();
9813
10108
  init_src6();
9814
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
9815
- import path23 from "node:path";
10109
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
10110
+ import path24 from "node:path";
9816
10111
 
9817
10112
  // packages/cli/src/assets-module.ts
9818
10113
  init_src();
@@ -10148,8 +10443,8 @@ async function runGenerationPipeline(input) {
10148
10443
  });
10149
10444
  const written = [];
10150
10445
  if (!input.dryRun) {
10151
- const dir = path23.resolve(input.outDir, semantics.componentName);
10152
- mkdirSync4(dir, { recursive: true });
10446
+ const dir = path24.resolve(input.outDir, semantics.componentName);
10447
+ mkdirSync5(dir, { recursive: true });
10153
10448
  const files = {
10154
10449
  // Bundle-local tokens: THE emission the component's CSS resolves
10155
10450
  // against — the same artifact the verify harness injects. A preview
@@ -10172,14 +10467,14 @@ async function runGenerationPipeline(input) {
10172
10467
  `
10173
10468
  };
10174
10469
  for (const [name, content] of Object.entries(files)) {
10175
- const filePath = path23.join(dir, name);
10176
- writeFileSync7(filePath, content);
10470
+ const filePath = path24.join(dir, name);
10471
+ writeFileSync8(filePath, content);
10177
10472
  written.push(filePath);
10178
10473
  }
10179
10474
  for (const artifact of emitTokenArtifacts(input.mapping)) {
10180
- const filePath = path23.resolve(input.outDir, artifact.path);
10181
- mkdirSync4(path23.dirname(filePath), { recursive: true });
10182
- writeFileSync7(filePath, artifact.content);
10475
+ const filePath = path24.resolve(input.outDir, artifact.path);
10476
+ mkdirSync5(path24.dirname(filePath), { recursive: true });
10477
+ writeFileSync8(filePath, artifact.content);
10183
10478
  written.push(filePath);
10184
10479
  }
10185
10480
  }
@@ -10237,7 +10532,7 @@ var GENERATE_DESCRIPTION = {
10237
10532
  function resolveProvidedSource(flags, contextFile) {
10238
10533
  let raw;
10239
10534
  try {
10240
- raw = readFileSync14(contextFile, "utf8");
10535
+ raw = readFileSync15(contextFile, "utf8");
10241
10536
  } catch {
10242
10537
  fail(flags, ExitCode.InputValidation, {
10243
10538
  error: `Cannot read context file "${contextFile}".`,
@@ -10271,6 +10566,7 @@ function resolveSource(flags, url) {
10271
10566
  });
10272
10567
  }
10273
10568
  async function runGenerate(url, flags) {
10569
+ requireEntitlement(flags);
10274
10570
  if (flags.describe) {
10275
10571
  printDescription(GENERATE_DESCRIPTION);
10276
10572
  return;
@@ -10356,11 +10652,11 @@ token mapping (${mapping.flat.length} variables):
10356
10652
  let initialCode;
10357
10653
  let initialSemantics;
10358
10654
  try {
10359
- if (existsSync17(flags.out)) {
10360
- for (const entry of readdirSync3(flags.out)) {
10655
+ if (existsSync18(flags.out)) {
10656
+ for (const entry of readdirSync4(flags.out)) {
10361
10657
  const cjPath = `${flags.out}/${entry}/component.json`;
10362
- if (!existsSync17(cjPath)) continue;
10363
- const cj = JSON.parse(readFileSync14(cjPath, "utf8"));
10658
+ if (!existsSync18(cjPath)) continue;
10659
+ const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
10364
10660
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
10365
10661
  previousApi = JSON.stringify({
10366
10662
  componentName: cj.name,
@@ -10368,14 +10664,14 @@ token mapping (${mapping.flat.length} variables):
10368
10664
  });
10369
10665
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
10370
10666
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
10371
- if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
10667
+ if (flags.refine && existsSync18(tsxPath) && existsSync18(cssPath)) {
10372
10668
  initialCode = {
10373
- tsx: readFileSync14(tsxPath, "utf8"),
10374
- css: readFileSync14(cssPath, "utf8")
10669
+ tsx: readFileSync15(tsxPath, "utf8"),
10670
+ css: readFileSync15(cssPath, "utf8")
10375
10671
  };
10376
10672
  const semPath = `${flags.out}/${entry}/semantics.json`;
10377
- if (existsSync17(semPath)) {
10378
- initialSemantics = JSON.parse(readFileSync14(semPath, "utf8"));
10673
+ if (existsSync18(semPath)) {
10674
+ initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
10379
10675
  }
10380
10676
  }
10381
10677
  break;
@@ -10544,6 +10840,12 @@ function buildProgram() {
10544
10840
  const local = cmd.opts();
10545
10841
  await runDoctor({ ...flags, mcpUrl: local["mcpUrl"] });
10546
10842
  });
10843
+ program.command("activate").description("Activate Tendril on this machine (browser approval; nothing to do on pre-launch builds).").option("--service-url <url>", "override the entitlement service (tests only)").action(async (_o, cmd) => {
10844
+ const flags = globalFlags(cmd);
10845
+ const local = cmd.opts();
10846
+ const { runActivate: runActivate2 } = await Promise.resolve().then(() => (init_activate(), activate_exports));
10847
+ await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
10848
+ });
10547
10849
  const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
10548
10850
  record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").requiredOption("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--default <axis=value...>", "explicit axis default, e.g. --default State=Rest (repeatable; persisted to the manifest; may re-plan an unrecorded set)").option("--component-set <name>", "when the metadata holds several component sets, record only this one (name = the set label in the plan error)").option("--sample", "cost sampling: anchor + one-factor + conflict crosses only (the FULL variant matrix is the default; sampling is blind to multi-axis interactions)").action(async (_o, cmd) => {
10549
10851
  const flags = globalFlags(cmd.parent.parent);