@tendrilapp/cli 0.1.14 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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();
@@ -5118,6 +5126,107 @@ var init_output = __esm({
5118
5126
  }
5119
5127
  });
5120
5128
 
5129
+ // packages/cli/src/entitlement.ts
5130
+ import { chmodSync, existsSync as existsSync16, mkdirSync as mkdirSync4, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "node:fs";
5131
+ import crypto from "node:crypto";
5132
+ import os3 from "node:os";
5133
+ import path21 from "node:path";
5134
+ function entitlementPath() {
5135
+ return process.env["TENDRIL_ENTITLEMENT_PATH"] ?? path21.join(os3.homedir(), ".tendril", "entitlement.json");
5136
+ }
5137
+ function readStoredEntitlement(file = entitlementPath()) {
5138
+ if (!existsSync16(file)) return void 0;
5139
+ try {
5140
+ const parsed = JSON.parse(readFileSync13(file, "utf8"));
5141
+ if (typeof parsed.token !== "string" || typeof parsed.lastRefreshAt !== "number") return void 0;
5142
+ return { token: parsed.token, lastRefreshAt: parsed.lastRefreshAt };
5143
+ } catch {
5144
+ return void 0;
5145
+ }
5146
+ }
5147
+ function writeStoredEntitlement(stored, file = entitlementPath()) {
5148
+ mkdirSync4(path21.dirname(file), { recursive: true });
5149
+ writeFileSync7(file, `${JSON.stringify(stored, null, 2)}
5150
+ `);
5151
+ chmodSync(file, 384);
5152
+ }
5153
+ function parseEntitlementToken(token) {
5154
+ if (!token.startsWith(ENT_PREFIX)) return { error: "not a tendril entitlement token" };
5155
+ const parts = token.slice(ENT_PREFIX.length).split(".");
5156
+ if (parts.length !== 2 || parts[0] === "" || parts[1] === "") return { error: "malformed token (expected payload.signature)" };
5157
+ let claims;
5158
+ try {
5159
+ claims = JSON.parse(b64urlDecode(parts[0]).toString("utf8"));
5160
+ } catch {
5161
+ return { error: "payload is not valid JSON" };
5162
+ }
5163
+ if (typeof claims.sub !== "string" || typeof claims.plan !== "string" || typeof claims.iat !== "number" || typeof claims.exp !== "number" || typeof claims.kid !== "string") {
5164
+ return { error: "payload is missing required claims" };
5165
+ }
5166
+ return { claims, signedData: b64urlDecode(parts[0]), signature: b64urlDecode(parts[1]) };
5167
+ }
5168
+ function checkEntitlement(opts = {}) {
5169
+ const keys = opts.keys ?? PUBLIC_KEYS;
5170
+ if (Object.keys(keys).length === 0) return { ok: true, mode: "pre-launch" };
5171
+ const now = opts.now ?? Date.now();
5172
+ const stored = "stored" in opts ? opts.stored : readStoredEntitlement();
5173
+ if (stored === void 0) {
5174
+ 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 };
5175
+ }
5176
+ const parsed = parseEntitlementToken(stored.token);
5177
+ if ("error" in parsed) {
5178
+ return { ok: false, code: "entitlement-invalid", error: `stored entitlement is unreadable: ${parsed.error}`, remediation: ACTIVATE_REMEDIATION };
5179
+ }
5180
+ const pem = keys[parsed.claims.kid];
5181
+ const valid = pem !== void 0 && (() => {
5182
+ try {
5183
+ return crypto.verify(null, parsed.signedData, crypto.createPublicKey(pem), parsed.signature);
5184
+ } catch {
5185
+ return false;
5186
+ }
5187
+ })();
5188
+ if (!valid) {
5189
+ return { ok: false, code: "entitlement-invalid", error: "stored entitlement failed signature verification", remediation: ACTIVATE_REMEDIATION };
5190
+ }
5191
+ if (now > parsed.claims.exp + TOLERANCE_MS) {
5192
+ return {
5193
+ ok: false,
5194
+ code: "entitlement-expired",
5195
+ error: "entitlement expired (and the offline tolerance window has passed)",
5196
+ remediation: `Reconnect and ${ACTIVATE_REMEDIATION}`
5197
+ };
5198
+ }
5199
+ if (now < stored.lastRefreshAt - CLOCK_ROLLBACK_MS) {
5200
+ return {
5201
+ ok: false,
5202
+ code: "entitlement-clock",
5203
+ error: "system clock sits more than a day before the last entitlement refresh",
5204
+ remediation: `Fix the system clock, then ${ACTIVATE_REMEDIATION}`
5205
+ };
5206
+ }
5207
+ return { ok: true, mode: "active", claims: parsed.claims, stale: now > parsed.claims.exp };
5208
+ }
5209
+ function requireEntitlement(flags) {
5210
+ const status = checkEntitlement();
5211
+ if (!status.ok) {
5212
+ fail(flags, ExitCode.Auth, { error: status.error, code: status.code, remediation: status.remediation });
5213
+ }
5214
+ }
5215
+ var ENT_PREFIX, PUBLIC_KEYS, TOLERANCE_MS, CLOCK_ROLLBACK_MS, b64urlDecode, ACTIVATE_REMEDIATION;
5216
+ var init_entitlement = __esm({
5217
+ "packages/cli/src/entitlement.ts"() {
5218
+ "use strict";
5219
+ init_src3();
5220
+ init_output();
5221
+ ENT_PREFIX = "tendril-ent.v1.";
5222
+ PUBLIC_KEYS = {};
5223
+ TOLERANCE_MS = 24 * 60 * 60 * 1e3;
5224
+ CLOCK_ROLLBACK_MS = 24 * 60 * 60 * 1e3;
5225
+ b64urlDecode = (s) => Buffer.from(s, "base64url");
5226
+ ACTIVATE_REMEDIATION = "Run `tendril activate` in your terminal (a browser approval \u2014 never paste license material into an agent chat).";
5227
+ }
5228
+ });
5229
+
5121
5230
  // packages/llm/src/model-config.ts
5122
5231
  import { z as z6 } from "zod";
5123
5232
  function resolveModel(config, requestedId) {
@@ -6309,6 +6418,100 @@ var init_src6 = __esm({
6309
6418
  }
6310
6419
  });
6311
6420
 
6421
+ // packages/cli/src/commands/activate.ts
6422
+ var activate_exports = {};
6423
+ __export(activate_exports, {
6424
+ ENTITLEMENT_SERVICE_URL: () => ENTITLEMENT_SERVICE_URL,
6425
+ runActivate: () => runActivate
6426
+ });
6427
+ async function runActivate(flags) {
6428
+ const base = flags.serviceUrl ?? ENTITLEMENT_SERVICE_URL;
6429
+ if (base === void 0) {
6430
+ fail(flags, ExitCode.Auth, {
6431
+ error: "the Tendril entitlement service is not live yet (pre-launch build) \u2014 there is nothing to activate against",
6432
+ code: "entitlement-service-unavailable",
6433
+ remediation: "Nothing to do: pre-launch builds run record/generate without activation. This command becomes meaningful at launch."
6434
+ });
6435
+ }
6436
+ let device;
6437
+ try {
6438
+ const res = await fetch(new URL("/v1/device/code", base), { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
6439
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6440
+ device = await res.json();
6441
+ if (typeof device.deviceCode !== "string" || typeof device.userCode !== "string" || typeof device.verificationUri !== "string") throw new Error("malformed device-code response");
6442
+ } catch (err) {
6443
+ fail(flags, ExitCode.Auth, {
6444
+ error: `could not start activation: ${err instanceof Error ? err.message : String(err)}`,
6445
+ code: "entitlement-service-unreachable",
6446
+ remediation: "Check your connection and retry `tendril activate`."
6447
+ });
6448
+ }
6449
+ process.stderr.write(`To activate Tendril, visit:
6450
+
6451
+ ${device.verificationUri}
6452
+
6453
+ and enter the code: ${device.userCode}
6454
+
6455
+ Waiting for approval\u2026
6456
+ `);
6457
+ const interval = Math.max(1e3, device.intervalMs ?? 5e3);
6458
+ const deadline = Date.now() + (device.expiresInMs ?? 10 * 60 * 1e3);
6459
+ let token;
6460
+ while (Date.now() < deadline) {
6461
+ await new Promise((r) => setTimeout(r, interval));
6462
+ const res = await fetch(new URL("/v1/device/token", base), {
6463
+ method: "POST",
6464
+ headers: { "content-type": "application/json" },
6465
+ body: JSON.stringify({ deviceCode: device.deviceCode })
6466
+ }).catch(() => void 0);
6467
+ if (res === void 0) continue;
6468
+ if (res.status === 428) continue;
6469
+ if (!res.ok) {
6470
+ fail(flags, ExitCode.Auth, {
6471
+ error: `activation was not approved (HTTP ${res.status})`,
6472
+ code: "entitlement-denied",
6473
+ remediation: "Retry `tendril activate`; if it persists, check the account's plan in the portal."
6474
+ });
6475
+ }
6476
+ token = (await res.json()).token;
6477
+ break;
6478
+ }
6479
+ if (token === void 0) {
6480
+ fail(flags, ExitCode.Auth, {
6481
+ error: "activation timed out before the browser approval arrived",
6482
+ code: "entitlement-timeout",
6483
+ remediation: "Run `tendril activate` again and complete the browser step within the shown window."
6484
+ });
6485
+ }
6486
+ const parsed = parseEntitlementToken(token);
6487
+ if ("error" in parsed) {
6488
+ fail(flags, ExitCode.Auth, {
6489
+ error: `service returned an unusable token: ${parsed.error}`,
6490
+ code: "entitlement-invalid",
6491
+ remediation: "Retry `tendril activate`; report this if it persists \u2014 it is a service-side fault."
6492
+ });
6493
+ }
6494
+ if (Object.keys(PUBLIC_KEYS).length > 0) {
6495
+ const status = checkEntitlement({ now: Date.now(), stored: { token, lastRefreshAt: Date.now() } });
6496
+ if (!status.ok) fail(flags, ExitCode.Auth, { error: `service returned a token this build cannot verify: ${status.error}`, code: status.code, remediation: status.remediation });
6497
+ }
6498
+ writeStoredEntitlement({ token, lastRefreshAt: Date.now() });
6499
+ emitData(flags, { activated: true, plan: parsed.claims.plan, sub: parsed.claims.sub, expiresAt: new Date(parsed.claims.exp).toISOString(), storedAt: entitlementPath() }, () => {
6500
+ process.stdout.write(`activated: plan "${parsed.claims.plan}" until ${new Date(parsed.claims.exp).toISOString().slice(0, 10)} (stored at ${entitlementPath()})
6501
+ `);
6502
+ });
6503
+ }
6504
+ var ENTITLEMENT_SERVICE_URL;
6505
+ var init_activate = __esm({
6506
+ "packages/cli/src/commands/activate.ts"() {
6507
+ "use strict";
6508
+ init_src3();
6509
+ init_entitlement();
6510
+ init_output();
6511
+ ENTITLEMENT_SERVICE_URL = void 0;
6512
+ }
6513
+ });
6514
+
6312
6515
  // packages/cli/src/commands/record.ts
6313
6516
  var record_exports = {};
6314
6517
  __export(record_exports, {
@@ -6325,11 +6528,11 @@ __export(record_exports, {
6325
6528
  runRecordPlan: () => runRecordPlan,
6326
6529
  runRecordStatus: () => runRecordStatus
6327
6530
  });
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";
6531
+ import { existsSync as existsSync19, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "node:fs";
6532
+ import path25 from "node:path";
6533
+ import { writeFileSync as writeFileSync9 } from "node:fs";
6331
6534
  function symbolsFromMetadataEnvelope(file, sourceFrame) {
6332
- const env = JSON.parse(readFileSync15(file, "utf8"));
6535
+ const env = JSON.parse(readFileSync16(file, "utf8"));
6333
6536
  const text = env.content.map((c) => c.text ?? "").join("\n");
6334
6537
  const symbols = [];
6335
6538
  const walk2 = (node, ancestor) => {
@@ -6359,6 +6562,7 @@ function instanceLeads(text) {
6359
6562
  return [...seen.entries()].map(([name, nodeId]) => ({ name, nodeId }));
6360
6563
  }
6361
6564
  function runRecordPlan(opts) {
6565
+ requireEntitlement(opts);
6362
6566
  const defaults = {};
6363
6567
  for (const spec of opts.defaultSpecs ?? []) {
6364
6568
  const eq = spec.indexOf("=");
@@ -6376,7 +6580,7 @@ function runRecordPlan(opts) {
6376
6580
  for (const spec of opts.metadataFiles) {
6377
6581
  const [file, frame] = spec.split("@");
6378
6582
  try {
6379
- const parsed = symbolsFromMetadataEnvelope(path24.resolve(file), frame);
6583
+ const parsed = symbolsFromMetadataEnvelope(path25.resolve(file), frame);
6380
6584
  symbols.push(...parsed.symbols);
6381
6585
  if (parsed.truncated) metadataTruncated = true;
6382
6586
  } catch (err) {
@@ -6410,7 +6614,7 @@ function runRecordPlan(opts) {
6410
6614
  const leads = opts.metadataFiles.flatMap((spec) => {
6411
6615
  const [file] = spec.split("@");
6412
6616
  try {
6413
- const env = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
6617
+ const env = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
6414
6618
  return instanceLeads(env.content.map((c) => c.text ?? "").join("\n"));
6415
6619
  } catch {
6416
6620
  return [];
@@ -6467,6 +6671,10 @@ function runRecordPlan(opts) {
6467
6671
  ...toppedUp !== void 0 ? { toppedUp } : {},
6468
6672
  notRecorded: manifest.notRecorded ?? null,
6469
6673
  figmaCallEstimate: { reps: manifest.reps.length, calls: `~${callLow}\u2013${callHigh}` },
6674
+ // Cold-start fix (run 7): the first instruction rides the plan
6675
+ // response, so record_next is never needed to begin — it exists
6676
+ // only for resuming.
6677
+ next: nextPayload(opts.setDir),
6470
6678
  ...toConfirm.length > 0 ? {
6471
6679
  defaultsToConfirm: {
6472
6680
  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 +6719,7 @@ function nextPayload(setDir) {
6511
6719
  const instruction = nextInstruction(setDir);
6512
6720
  const status = sessionStatus(setDir);
6513
6721
  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"))) {
6722
+ if (instruction === null && !existsSync19(path25.join(setDir, "get_variable_defs.json"))) {
6515
6723
  const manifest = loadManifest(setDir);
6516
6724
  const frameNode = Object.keys(manifest.sourceFrames ?? {})[0] ?? manifest.reps[0]?.nodeId ?? "";
6517
6725
  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 +6810,7 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6602
6810
  const skipped = [];
6603
6811
  const failed = [];
6604
6812
  for (const { url, name } of assetUrlsFromEnvelopeText(envelopeText2)) {
6605
- if (existsSync18(path24.join(setDir, rep, name))) {
6813
+ if (existsSync19(path25.join(setDir, rep, name))) {
6606
6814
  skipped.push(name);
6607
6815
  continue;
6608
6816
  }
@@ -6624,16 +6832,16 @@ async function autoFetchAssets(setDir, rep, envelopeText2) {
6624
6832
  }
6625
6833
  function rawEnvelopeFromFile(file, parts) {
6626
6834
  if (parts) {
6627
- const blocks = JSON.parse(readFileSync15(path24.resolve(file), "utf8"));
6835
+ const blocks = JSON.parse(readFileSync16(path25.resolve(file), "utf8"));
6628
6836
  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
6837
  return { content: blocks.map((text) => ({ type: "text", text })) };
6630
6838
  }
6631
- return { content: [{ type: "text", text: readFileSync15(path24.resolve(file), "utf8") }] };
6839
+ return { content: [{ type: "text", text: readFileSync16(path25.resolve(file), "utf8") }] };
6632
6840
  }
6633
6841
  async function runRecordIngest(opts) {
6634
6842
  let payload;
6635
6843
  try {
6636
- payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync15(path24.resolve(opts.file), "utf8"));
6844
+ payload = opts.rawParts === true || opts.raw === true ? rawEnvelopeFromFile(opts.file, opts.rawParts === true) : JSON.parse(readFileSync16(path25.resolve(opts.file), "utf8"));
6637
6845
  } catch (err) {
6638
6846
  fail(opts, ExitCode.InputValidation, {
6639
6847
  error: `cannot read envelope file: ${err instanceof Error ? err.message : String(err)}`,
@@ -6657,7 +6865,7 @@ async function runRecordIngest(opts) {
6657
6865
  remediation: "Save the get_variable_defs response verbatim as a text envelope."
6658
6866
  });
6659
6867
  }
6660
- writeFileSync8(path24.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
6868
+ writeFileSync9(path25.join(opts.setDir, "get_variable_defs.json"), `${JSON.stringify(payload, null, 1)}
6661
6869
  `);
6662
6870
  emitData(opts, { ingested: "get_variable_defs", rep: "__set__", setLevel: true, next: nextPayload(opts.setDir) }, () => {
6663
6871
  process.stdout.write("set-level get_variable_defs ingested\n");
@@ -6753,8 +6961,8 @@ async function runRecordIngestRep(opts) {
6753
6961
  }
6754
6962
  function runRecordAsset(opts) {
6755
6963
  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));
6964
+ const dir = path25.resolve(opts.dir);
6965
+ const names = readdirSync5(dir).filter((f) => /^asset-[\w.-]+\.(svg|png|jpe?g|webp|gif)$/i.test(f));
6758
6966
  if (names.length === 0) {
6759
6967
  fail(opts, ExitCode.InputValidation, {
6760
6968
  error: `no asset-*.<ext> files found in ${dir}`,
@@ -6765,7 +6973,7 @@ function runRecordAsset(opts) {
6765
6973
  const ingested = [];
6766
6974
  try {
6767
6975
  for (const name of names) {
6768
- ingestAsset(opts.setDir, opts.rep, name, readFileSync15(path24.join(dir, name)));
6976
+ ingestAsset(opts.setDir, opts.rep, name, readFileSync16(path25.join(dir, name)));
6769
6977
  ingested.push(name);
6770
6978
  }
6771
6979
  } catch (err) {
@@ -6789,7 +6997,7 @@ function runRecordAsset(opts) {
6789
6997
  });
6790
6998
  }
6791
6999
  try {
6792
- ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync15(path24.resolve(opts.file)));
7000
+ ingestAsset(opts.setDir, opts.rep, opts.name, readFileSync16(path25.resolve(opts.file)));
6793
7001
  emitData(opts, { rep: opts.rep, asset: opts.name }, () => {
6794
7002
  process.stdout.write(`ingested ${opts.rep}/${opts.name}
6795
7003
  `);
@@ -6817,7 +7025,7 @@ ${status.reps.filter((r) => r.missing.length > 0).length} rep(s) pending
6817
7025
  function runRecordFinish(opts) {
6818
7026
  const manifest = loadManifest(opts.setDir);
6819
7027
  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 };
7028
+ 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
7029
  emitData(opts, { derived, confirmed: opts.confirmRoles }, () => {
6822
7030
  process.stdout.write(`derived mains: ${derived.main.join(", ") || "(none)"}
6823
7031
  `);
@@ -6844,7 +7052,7 @@ function runRecordFinish(opts) {
6844
7052
  });
6845
7053
  }
6846
7054
  const updated = { ...manifest, roles };
6847
- writeFileSync8(path24.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
7055
+ writeFileSync9(path25.join(opts.setDir, "recording-set.json"), `${JSON.stringify(updated, null, 1)}
6848
7056
  `);
6849
7057
  if (!opts.json) process.stdout.write("roles written to recording-set.json\n");
6850
7058
  }
@@ -6855,6 +7063,7 @@ var init_record = __esm({
6855
7063
  init_src3();
6856
7064
  init_src();
6857
7065
  init_output();
7066
+ init_entitlement();
6858
7067
  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
7068
  isAutoFetchAssetUrl = (url) => isFigmaAssetUrl(url) || isLocalAssetUrl(url);
6860
7069
  }
@@ -7047,8 +7256,8 @@ var init_engine_curated = __esm({
7047
7256
  });
7048
7257
 
7049
7258
  // 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";
7259
+ import { existsSync as existsSync20, mkdirSync as mkdirSync6, readFileSync as readFileSync17, renameSync, writeFileSync as writeFileSync10 } from "node:fs";
7260
+ import path26 from "node:path";
7052
7261
  import { z as z11 } from "zod";
7053
7262
  function objective(scores, behaviors) {
7054
7263
  const vals = scores.map((s) => Math.min(s.similarity, s.inkRecall));
@@ -7079,9 +7288,9 @@ ${preludeLines.join("\n")}` : ""}
7079
7288
  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
7289
  }
7081
7290
  function archivePriorRun(outDir) {
7082
- if (!existsSync19(path25.join(outDir, "run-log.json")) && !existsSync19(path25.join(outDir, "loop-state.json"))) return void 0;
7291
+ if (!existsSync20(path26.join(outDir, "run-log.json")) && !existsSync20(path26.join(outDir, "loop-state.json"))) return void 0;
7083
7292
  let n = 1;
7084
- while (existsSync19(`${outDir}-prev-${n}`)) n += 1;
7293
+ while (existsSync20(`${outDir}-prev-${n}`)) n += 1;
7085
7294
  renameSync(outDir, `${outDir}-prev-${n}`);
7086
7295
  return `${outDir}-prev-${n}`;
7087
7296
  }
@@ -7090,14 +7299,14 @@ async function runEngineLoop(opts) {
7090
7299
  const plateau = opts.plateau ?? 2;
7091
7300
  const progress = opts.onProgress ?? (() => {
7092
7301
  });
7093
- const statePath = path25.join(opts.outDir, "loop-state.json");
7094
- const resuming = opts.resume === true && existsSync19(statePath);
7302
+ const statePath = path26.join(opts.outDir, "loop-state.json");
7303
+ const resuming = opts.resume === true && existsSync20(statePath);
7095
7304
  if (!resuming) {
7096
7305
  const archived = archivePriorRun(opts.outDir);
7097
7306
  if (archived !== void 0) progress(`previous run archived to ${archived}`);
7098
7307
  }
7099
- mkdirSync5(opts.outDir, { recursive: true });
7100
- const scratch = path25.join(opts.outDir, ".candidate");
7308
+ mkdirSync6(opts.outDir, { recursive: true });
7309
+ const scratch = path26.join(opts.outDir, ".candidate");
7101
7310
  let attempts = [];
7102
7311
  let log = [];
7103
7312
  let best;
@@ -7105,7 +7314,7 @@ async function runEngineLoop(opts) {
7105
7314
  let nonAccepted = 0;
7106
7315
  let stopReason = "max-iterations";
7107
7316
  if (resuming) {
7108
- const restored = LoopStateSchema.parse(JSON.parse(readFileSync16(statePath, "utf8")));
7317
+ const restored = LoopStateSchema.parse(JSON.parse(readFileSync17(statePath, "utf8")));
7109
7318
  attempts = restored.attempts;
7110
7319
  log = restored.iterations;
7111
7320
  spentUsd = restored.spentUsd;
@@ -7120,12 +7329,12 @@ async function runEngineLoop(opts) {
7120
7329
  progress(`resumed: ${log.length} iteration(s), $${spentUsd.toFixed(3)} spent, best pass=${best?.objective[0] ?? 0}`);
7121
7330
  }
7122
7331
  const persist = () => {
7123
- writeFileSync9(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
7332
+ writeFileSync10(statePath, `${JSON.stringify({ version: 1, spentUsd, attempts, iterations: log }, null, 1)}
7124
7333
  `);
7125
7334
  };
7126
7335
  const writeCandidate = (files) => {
7127
- mkdirSync5(scratch, { recursive: true });
7128
- for (const [name, content] of Object.entries(files)) writeFileSync9(path25.join(scratch, name), content);
7336
+ mkdirSync6(scratch, { recursive: true });
7337
+ for (const [name, content] of Object.entries(files)) writeFileSync10(path26.join(scratch, name), content);
7129
7338
  };
7130
7339
  const scoreCandidate = async (candidate, iter, usd, modelMs) => {
7131
7340
  writeCandidate(candidate.files);
@@ -7183,8 +7392,8 @@ async function runEngineLoop(opts) {
7183
7392
  const usd = candidate.usage?.usd ?? 0;
7184
7393
  spentUsd += usd;
7185
7394
  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);
7395
+ mkdirSync6(path26.join(opts.outDir, "responses"), { recursive: true });
7396
+ writeFileSync10(path26.join(opts.outDir, "responses", `iter-${iter}.md`), candidate.raw);
7188
7397
  }
7189
7398
  if (candidate.files[opts.entry] === void 0 || candidate.files["styles.css"] === void 0) {
7190
7399
  const finish = candidate.usage?.finishReason ?? "?";
@@ -7210,10 +7419,10 @@ async function runEngineLoop(opts) {
7210
7419
  }
7211
7420
  }
7212
7421
  }
7213
- if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync9(path25.join(opts.outDir, name), content);
7422
+ if (best !== void 0) for (const [name, content] of Object.entries(best.files)) writeFileSync10(path26.join(opts.outDir, name), content);
7214
7423
  const final = best !== void 0 ? await opts.score(opts.outDir) : { scores: [], behaviors: [] };
7215
- writeFileSync9(
7216
- path25.join(opts.outDir, "run-log.json"),
7424
+ writeFileSync10(
7425
+ path26.join(opts.outDir, "run-log.json"),
7217
7426
  `${JSON.stringify(
7218
7427
  {
7219
7428
  ...opts.meta,
@@ -7280,8 +7489,8 @@ var init_loop2 = __esm({
7280
7489
  });
7281
7490
 
7282
7491
  // packages/generate/src/brief.ts
7283
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "node:fs";
7284
- import path26 from "node:path";
7492
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "node:fs";
7493
+ import path27 from "node:path";
7285
7494
  function singleAxes2(name) {
7286
7495
  const parsed = parseVariantAxes(name);
7287
7496
  if (parsed === void 0) return void 0;
@@ -7514,15 +7723,18 @@ function authorBehaviors(api, extras = {}) {
7514
7723
  return { behaviors, prelude: { controls: interactive ? ["> *"] : [], textInputs: [] }, disclosures };
7515
7724
  }
7516
7725
  function envelopeText(file) {
7517
- return envelopeFirstTextPart(JSON.parse(readFileSync17(file, "utf8")));
7726
+ return envelopeFirstTextPart(JSON.parse(readFileSync18(file, "utf8")));
7518
7727
  }
7519
7728
  function dismissEvidence(setDir, repSlugs) {
7520
7729
  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)) {
7730
+ const f = path27.join(setDir, slug, "get_design_context.json");
7731
+ if (!existsSync21(f)) continue;
7732
+ const text = envelopeText(f);
7733
+ 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);
7734
+ if (propHit !== null) return `emission prop "${propHit[1]}"`;
7735
+ for (const m of text.matchAll(/data-name="([^"]+)"/g)) {
7524
7736
  const norm = m[1].toLowerCase().replace(/[^a-z0-9]/g, "");
7525
- if (DISMISS_NAMES.has(norm)) return m[1];
7737
+ if (DISMISS_NAMES.has(norm)) return `layer ${JSON.stringify(m[1])}`;
7526
7738
  }
7527
7739
  }
7528
7740
  return void 0;
@@ -7560,13 +7772,13 @@ function recordedFontNeeds(setDir) {
7560
7772
  }
7561
7773
  };
7562
7774
  const manifest = loadManifest(setDir);
7563
- const setDefs = path26.join(setDir, "get_variable_defs.json");
7564
- if (existsSync20(setDefs)) fromDefs(envelopeText(setDefs));
7775
+ const setDefs = path27.join(setDir, "get_variable_defs.json");
7776
+ if (existsSync21(setDefs)) fromDefs(envelopeText(setDefs));
7565
7777
  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));
7778
+ const ctx = path27.join(setDir, rep.slug, "get_design_context.json");
7779
+ if (existsSync21(ctx)) fromEmission(envelopeText(ctx));
7780
+ const defs = path27.join(setDir, rep.slug, "get_variable_defs.json");
7781
+ if (existsSync21(defs)) fromDefs(envelopeText(defs));
7570
7782
  }
7571
7783
  return [...byFamily.entries()].map(([family, paired]) => {
7572
7784
  const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
@@ -7589,8 +7801,8 @@ function recordedTextSlots(setDir, repSlugs) {
7589
7801
  const propRep = [];
7590
7802
  const perRep = [];
7591
7803
  for (const slug of repSlugs) {
7592
- const f = path26.join(setDir, slug, "get_design_context.json");
7593
- if (!existsSync20(f)) continue;
7804
+ const f = path27.join(setDir, slug, "get_design_context.json");
7805
+ if (!existsSync21(f)) continue;
7594
7806
  const code = envelopeText(f);
7595
7807
  const props = /* @__PURE__ */ new Map();
7596
7808
  for (const m of code.matchAll(/[{,]\s*(\w+)\s*=\s*"((?:[^"\\]|\\.)*)"/g)) {
@@ -7606,12 +7818,17 @@ function recordedTextSlots(setDir, repSlugs) {
7606
7818
  if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
7607
7819
  texts.push(t);
7608
7820
  }
7821
+ for (const m of code.matchAll(/>\{[`'"]([^`'"]+)[`'"]\}</g)) {
7822
+ const t = decodeXmlEntities(m[1]).trim();
7823
+ if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
7824
+ texts.push(t);
7825
+ }
7609
7826
  perRep.push({ slug, texts });
7610
7827
  }
7611
7828
  const axisValuesBySlug = /* @__PURE__ */ new Map();
7612
7829
  for (const slug of repSlugs) {
7613
- const metaFile = path26.join(setDir, slug, "get_metadata.json");
7614
- if (!existsSync20(metaFile)) continue;
7830
+ const metaFile = path27.join(setDir, slug, "get_metadata.json");
7831
+ if (!existsSync21(metaFile)) continue;
7615
7832
  const name = symbolName(envelopeText(metaFile));
7616
7833
  if (name === void 0) continue;
7617
7834
  const values = /* @__PURE__ */ new Set();
@@ -7647,7 +7864,7 @@ function recordedTextSlots(setDir, repSlugs) {
7647
7864
  }
7648
7865
  const visibleIn = perRep.filter((r) => {
7649
7866
  const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
7650
- return r.texts.some((t) => t.includes(v));
7867
+ return r.texts.some((t) => t.includes(v)) || r.texts.join(" ").replace(/\s+/g, " ").includes(v.replace(/\s+/g, " ").trim());
7651
7868
  }).map((r) => r.slug);
7652
7869
  return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn };
7653
7870
  });
@@ -7708,8 +7925,8 @@ function authorTaskFromSet(setDir, opts = {}) {
7708
7925
  const poses = [];
7709
7926
  const missing = [];
7710
7927
  for (const rep of manifest.reps) {
7711
- const metaFile = path26.join(setDir, rep.slug, "get_metadata.json");
7712
- if (!existsSync20(metaFile)) {
7928
+ const metaFile = path27.join(setDir, rep.slug, "get_metadata.json");
7929
+ if (!existsSync21(metaFile)) {
7713
7930
  missing.push(rep.slug);
7714
7931
  continue;
7715
7932
  }
@@ -7723,8 +7940,8 @@ function authorTaskFromSet(setDir, opts = {}) {
7723
7940
  if (missing.length > 0) {
7724
7941
  throw new Error(`recording set ${setDir} is missing metadata for ${missing.length} rep(s): ${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", \u2026" : ""}`);
7725
7942
  }
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);
7943
+ const setMeta = path27.join(setDir, "get_metadata.json");
7944
+ const latticeNames = manifest.latticeNames ?? (existsSync21(setMeta) ? [...envelopeText(setMeta).matchAll(/name="([^"]*)"/g)].map((m) => decodeXmlEntities(m[1])) : void 0);
7728
7945
  const defaults = { ...manifest.defaults ?? {}, ...opts.defaults ?? {} };
7729
7946
  const recordedFonts = recordedFontFamilies(setDir);
7730
7947
  const textSlots = recordedTextSlots(setDir, manifest.reps.map((r) => r.slug));
@@ -7746,7 +7963,7 @@ function authorTaskFromSet(setDir, opts = {}) {
7746
7963
  }).filter((x) => x !== void 0);
7747
7964
  const { behaviors, prelude, disclosures } = authorBehaviors(api, { ...sentinels.length > 0 ? { sentinels } : {} });
7748
7965
  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`);
7966
+ disclosures.push(`dismiss affordance detected from the recording (${dismissName}) \u2014 onDismiss authored; dismiss-notifies-and-commits enforces the notification contract`);
7750
7967
  }
7751
7968
  for (const combo of api.syntheticCombos) {
7752
7969
  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`);
@@ -7781,7 +7998,7 @@ ALL prose instructions live ABOVE the task payload \u2014 the payload contains o
7781
7998
 
7782
7999
  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
8000
 
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.
8001
+ 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
8002
 
7786
8003
  RECORDED BOXES ARE TRUTH even when inconsistent: the same string may
7787
8004
  have different recorded widths across variants (designer resizing) \u2014 a
@@ -7869,10 +8086,10 @@ var init_brief = __esm({
7869
8086
  });
7870
8087
 
7871
8088
  // 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";
8089
+ import { existsSync as existsSync22, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
8090
+ import path28 from "node:path";
7874
8091
  function repText(set, rep, tool) {
7875
- const env = JSON.parse(readFileSync18(path27.join(set, rep, `${tool}.json`), "utf8"));
8092
+ const env = JSON.parse(readFileSync19(path28.join(set, rep, `${tool}.json`), "utf8"));
7876
8093
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
7877
8094
  }
7878
8095
  function stripFigmaInstructions(emission) {
@@ -7933,17 +8150,17 @@ DROPPED from the map, untrustworthy in the source export \u2014 for these, each
7933
8150
  function buildSegments(task, mode = "fenced") {
7934
8151
  const SET = task.set;
7935
8152
  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"))) || "{}";
8153
+ if (existsSync22(path28.join(SET, "get_variable_defs.json"))) {
8154
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync19(path28.join(SET, "get_variable_defs.json"), "utf8"))) || "{}";
7938
8155
  try {
7939
8156
  rawDefs = JSON.parse(text);
7940
8157
  } catch {
7941
8158
  }
7942
8159
  } else {
7943
8160
  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"))) || "{}";
8161
+ const f = path28.join(SET, cfg.rep, "get_variable_defs.json");
8162
+ if (!existsSync22(f)) continue;
8163
+ const text = envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) || "{}";
7947
8164
  try {
7948
8165
  for (const [k, v] of Object.entries(JSON.parse(text))) rawDefs[k] ??= v;
7949
8166
  } catch {
@@ -7951,8 +8168,8 @@ function buildSegments(task, mode = "fenced") {
7951
8168
  }
7952
8169
  }
7953
8170
  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"))) : "";
8171
+ const f = path28.join(SET, cfg.rep, "get_design_context.json");
8172
+ return existsSync22(f) ? envelopeFirstTextPart(JSON.parse(readFileSync19(f, "utf8"))) : "";
7956
8173
  });
7957
8174
  const { map, note } = cleanTokenMap(rawDefs, emissionTexts);
7958
8175
  const defs = JSON.stringify(map, null, 1);
@@ -7967,9 +8184,9 @@ ${defs}
7967
8184
  for (const cfg of task.configs) {
7968
8185
  const meta = stripFigmaInstructions(repText(SET, cfg.rep, "get_metadata"));
7969
8186
  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}:
8187
+ const assets = readdirSync6(path28.join(SET, cfg.rep)).filter((f) => f.startsWith("asset-") && f.endsWith(".svg")).map((f) => `asset ${f}:
7971
8188
  \`\`\`svg
7972
- ${readFileSync18(path27.join(SET, cfg.rep, f), "utf8")}
8189
+ ${readFileSync19(path28.join(SET, cfg.rep, f), "utf8")}
7973
8190
  \`\`\``).join("\n");
7974
8191
  parts.push(`
7975
8192
  ## Config ${cfg.rep} \u2192 ${cfg.component} ${JSON.stringify(cfg.props)}
@@ -8062,8 +8279,8 @@ var init_adapter = __esm({
8062
8279
 
8063
8280
  // packages/generate/src/bundle-emit.ts
8064
8281
  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";
8282
+ import { existsSync as existsSync23, readFileSync as readFileSync20, readdirSync as readdirSync7, writeFileSync as writeFileSync11 } from "node:fs";
8283
+ import path29 from "node:path";
8067
8284
  function pinFromConfigs(configs) {
8068
8285
  const domains = /* @__PURE__ */ new Map();
8069
8286
  const kinds = /* @__PURE__ */ new Map();
@@ -8112,22 +8329,22 @@ function cssFontFamilies(css) {
8112
8329
  return [...out];
8113
8330
  }
8114
8331
  function countLatticeSymbols(setDir) {
8115
- const manifestFile = path28.join(setDir, "recording-set.json");
8116
- if (existsSync22(manifestFile)) {
8332
+ const manifestFile = path29.join(setDir, "recording-set.json");
8333
+ if (existsSync23(manifestFile)) {
8117
8334
  try {
8118
- const lattice = JSON.parse(readFileSync19(manifestFile, "utf8")).latticeNames;
8335
+ const lattice = JSON.parse(readFileSync20(manifestFile, "utf8")).latticeNames;
8119
8336
  if (lattice !== void 0 && lattice.length > 0) return lattice.length;
8120
8337
  } catch {
8121
8338
  }
8122
8339
  }
8123
8340
  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));
8341
+ path29.join(setDir, "get_metadata.json"),
8342
+ ...existsSync23(setDir) ? readdirSync7(setDir).filter((f) => /^get_metadata-.*\.json$/.test(f)).map((f) => path29.join(setDir, f)) : []
8343
+ ].filter((f) => existsSync23(f));
8127
8344
  if (files.length === 0) return null;
8128
8345
  let count = 0;
8129
8346
  for (const f of files) {
8130
- const text = envelopeTextContent(JSON.parse(readFileSync19(f, "utf8")));
8347
+ const text = envelopeTextContent(JSON.parse(readFileSync20(f, "utf8")));
8131
8348
  count += [...text.matchAll(/name="([^"]*)"/g)].filter((m) => m[1].includes("=")).length;
8132
8349
  }
8133
8350
  return count > 0 ? count : null;
@@ -8135,21 +8352,21 @@ function countLatticeSymbols(setDir) {
8135
8352
  function recordingSetHash(setDir, configs) {
8136
8353
  const relPaths = [];
8137
8354
  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);
8355
+ if (existsSync23(path29.join(setDir, name))) relPaths.push(name);
8139
8356
  }
8140
8357
  for (const cfg of configs) {
8141
8358
  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}`);
8359
+ if (existsSync23(path29.join(setDir, cfg.rep, f))) relPaths.push(`${cfg.rep}/${f}`);
8143
8360
  }
8144
- if (existsSync22(path28.join(setDir, cfg.rep))) {
8145
- for (const asset of readdirSync6(path28.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
8361
+ if (existsSync23(path29.join(setDir, cfg.rep))) {
8362
+ for (const asset of readdirSync7(path29.join(setDir, cfg.rep)).filter((f) => f.startsWith("asset-"))) {
8146
8363
  relPaths.push(`${cfg.rep}/${asset}`);
8147
8364
  }
8148
8365
  }
8149
8366
  }
8150
8367
  return hashRecordingSet(
8151
8368
  relPaths,
8152
- (p) => new Uint8Array(readFileSync19(path28.join(setDir, p))),
8369
+ (p) => new Uint8Array(readFileSync20(path29.join(setDir, p))),
8153
8370
  (chunks) => {
8154
8371
  const h = createHash3("sha256");
8155
8372
  for (const c of chunks) h.update(c);
@@ -8167,8 +8384,8 @@ function emitBundleV1(opts) {
8167
8384
  const lattice = countLatticeSymbols(opts.task.set);
8168
8385
  const interaction = opts.behaviors.filter((b) => !b.id.startsWith("prelude:"));
8169
8386
  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"));
8387
+ const cssFiles = ["styles.css", "tokens.css"].map((f) => path29.join(opts.bundleDir, f)).filter((f) => existsSync23(f));
8388
+ const families = cssFontFamilies(cssFiles.map((f) => readFileSync20(f, "utf8")).join("\n"));
8172
8389
  const requiredFonts = requiredFontsManifest(families, opts.fontCacheDir).map((f) => ({
8173
8390
  family: f.family,
8174
8391
  weight: f.weight,
@@ -8197,7 +8414,7 @@ function emitBundleV1(opts) {
8197
8414
  // resolvable via verify's --set override).
8198
8415
  path: (() => {
8199
8416
  const base = process.env["INIT_CWD"] ?? process.cwd();
8200
- const rel = path28.relative(base, opts.task.set);
8417
+ const rel = path29.relative(base, opts.task.set);
8201
8418
  return rel !== "" && !rel.startsWith("..") ? rel : opts.task.set;
8202
8419
  })(),
8203
8420
  component: opts.componentName,
@@ -8221,16 +8438,16 @@ function emitBundleV1(opts) {
8221
8438
  })
8222
8439
  };
8223
8440
  const written = [];
8224
- const manifestPath2 = path28.join(opts.bundleDir, "component.json");
8225
- writeFileSync10(manifestPath2, `${JSON.stringify(manifest, null, 2)}
8441
+ const manifestPath2 = path29.join(opts.bundleDir, "component.json");
8442
+ writeFileSync11(manifestPath2, `${JSON.stringify(manifest, null, 2)}
8226
8443
  `);
8227
8444
  written.push(manifestPath2);
8228
- const stylesPath = path28.join(opts.bundleDir, "styles.css");
8229
- if (existsSync22(stylesPath)) {
8445
+ const stylesPath = path29.join(opts.bundleDir, "styles.css");
8446
+ if (existsSync23(stylesPath)) {
8230
8447
  const comment = cssProvenanceComment({ pass, scored: statuses.length, certified, latticeConfigs: lattice });
8231
- const current = readFileSync19(stylesPath, "utf8");
8448
+ const current = readFileSync20(stylesPath, "utf8");
8232
8449
  const stripped = current.replace(/^\/\* tendril bundle v\d+ [^]*?\*\/\n/, "");
8233
- writeFileSync10(stylesPath, `${comment}
8450
+ writeFileSync11(stylesPath, `${comment}
8234
8451
  ${stripped}`);
8235
8452
  written.push(stylesPath);
8236
8453
  }
@@ -8278,8 +8495,8 @@ __export(fonts_exports, {
8278
8495
  runFontsResolveSet: () => runFontsResolveSet,
8279
8496
  runFontsStatus: () => runFontsStatus
8280
8497
  });
8281
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "node:fs";
8282
- import path29 from "node:path";
8498
+ import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
8499
+ import path30 from "node:path";
8283
8500
  async function runFontsResolve(opts) {
8284
8501
  const result = await resolveFonts(opts.family, opts.weights, opts.cacheDir);
8285
8502
  emitData(opts, result, () => {
@@ -8294,7 +8511,7 @@ async function runFontsResolve(opts) {
8294
8511
  }
8295
8512
  }
8296
8513
  async function runFontsResolveSet(opts) {
8297
- const setDir = path29.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
8514
+ const setDir = path30.resolve(process.env["INIT_CWD"] ?? process.cwd(), opts.set);
8298
8515
  let needs = [];
8299
8516
  try {
8300
8517
  needs = recordedFontNeeds(setDir);
@@ -8343,16 +8560,16 @@ async function runFontsResolveSet(opts) {
8343
8560
  }
8344
8561
  }
8345
8562
  function runFontsStatus(opts) {
8346
- const manifestPath2 = path29.join(opts.cacheDir, "manifest.json");
8347
- if (!existsSync23(manifestPath2)) {
8563
+ const manifestPath2 = path30.join(opts.cacheDir, "manifest.json");
8564
+ if (!existsSync24(manifestPath2)) {
8348
8565
  fail(opts, ExitCode.FontsUnproven, {
8349
8566
  error: `no font cache at ${opts.cacheDir}`,
8350
8567
  code: "fonts-unresolved",
8351
8568
  remediation: 'Run `tendril fonts resolve --set <recording-dir>` (or `tendril fonts resolve "<Family>" --weights 400 500 600`) first.'
8352
8569
  });
8353
8570
  }
8354
- const faces = JSON.parse(readFileSync20(manifestPath2, "utf8"));
8355
- const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path29.resolve(opts.lock), opts.cacheDir) : null;
8571
+ const faces = JSON.parse(readFileSync21(manifestPath2, "utf8"));
8572
+ const lockVerdicts = opts.lock !== void 0 ? checkFontLock(path30.resolve(opts.lock), opts.cacheDir) : null;
8356
8573
  emitData(opts, { faces, lockVerdicts }, () => {
8357
8574
  for (const f of faces) process.stdout.write(`cached ${f.family} ${f.weight} (${f.sha256.slice(0, 12)}\u2026)
8358
8575
  `);
@@ -8437,8 +8654,8 @@ __export(verify_exports, {
8437
8654
  interactionCoverage: () => interactionCoverage,
8438
8655
  runVerify: () => runVerify
8439
8656
  });
8440
- import { existsSync as existsSync24, readFileSync as readFileSync21 } from "node:fs";
8441
- import path30 from "node:path";
8657
+ import { existsSync as existsSync25, readFileSync as readFileSync22 } from "node:fs";
8658
+ import path31 from "node:path";
8442
8659
  function interactionCoverage(behaviors) {
8443
8660
  const interaction = behaviors.filter((b) => !b.id.startsWith("prelude:"));
8444
8661
  return {
@@ -8453,7 +8670,7 @@ function taskFromManifest(opts, manifest, setDir) {
8453
8670
  const adapterSlugs = Object.keys(manifest.propAdapter);
8454
8671
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
8455
8672
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
8456
- const registry = Object.values(TASKS).find((t) => path30.resolve(t.set) === path30.resolve(setDir));
8673
+ const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
8457
8674
  const authored = (() => {
8458
8675
  if (registry !== void 0) return void 0;
8459
8676
  try {
@@ -8485,19 +8702,19 @@ function taskFromManifest(opts, manifest, setDir) {
8485
8702
  }
8486
8703
  async function runVerify(opts) {
8487
8704
  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)) {
8705
+ const setOverride = opts.set !== void 0 ? path31.resolve(callerCwd, opts.set) : void 0;
8706
+ opts = { ...opts, bundleDir: path31.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
8707
+ if (!existsSync25(opts.bundleDir)) {
8491
8708
  fail(opts, ExitCode.InputValidation, {
8492
8709
  error: `bundle directory not found: ${opts.bundleDir}`,
8493
8710
  code: "bundle-missing",
8494
8711
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
8495
8712
  });
8496
8713
  }
8497
- const manifestPath2 = path30.join(opts.bundleDir, "component.json");
8714
+ const manifestPath2 = path31.join(opts.bundleDir, "component.json");
8498
8715
  let manifest;
8499
- if (existsSync24(manifestPath2)) {
8500
- const { manifest: parsed, issues } = readBundleManifest(readFileSync21(manifestPath2, "utf8"));
8716
+ if (existsSync25(manifestPath2)) {
8717
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync22(manifestPath2, "utf8"));
8501
8718
  if (issues.length > 0) {
8502
8719
  fail(opts, ExitCode.InputValidation, {
8503
8720
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -8524,21 +8741,21 @@ async function runVerify(opts) {
8524
8741
  task = registry;
8525
8742
  } else if (manifest !== void 0) {
8526
8743
  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);
8744
+ if (path31.isAbsolute(p)) return p;
8745
+ const fromRepo = path31.resolve(REPO_ROOT, p);
8746
+ if (existsSync25(fromRepo)) return fromRepo;
8747
+ return path31.resolve(callerCwd, p);
8531
8748
  };
8532
8749
  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))) {
8750
+ if (!existsSync25(path31.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path31.resolve(t.set) === path31.resolve(setDir))) {
8534
8751
  fail(opts, ExitCode.RecordingIncomplete, {
8535
8752
  error: `recording set not found or unmanifested: ${setDir}`,
8536
8753
  code: "recording-set-missing",
8537
8754
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
8538
8755
  });
8539
8756
  }
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"))) {
8757
+ const registry = Object.values(TASKS).find((t) => path31.resolve(t.set) === path31.resolve(setDir));
8758
+ if (registry !== void 0 && !existsSync25(path31.join(setDir, "recording-set.json"))) {
8542
8759
  const recordedSlugs = registry.configs.map((c) => c.rep);
8543
8760
  const adapterSlugs = Object.keys(manifest.propAdapter);
8544
8761
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -8562,9 +8779,9 @@ async function runVerify(opts) {
8562
8779
  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
8780
  }
8564
8781
  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)));
8782
+ const p = path31.join(opts.bundleDir, name);
8783
+ if (!existsSync25(p)) continue;
8784
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync22(p)));
8568
8785
  if (issues.length > 0) {
8569
8786
  fail(opts, ExitCode.InputValidation, {
8570
8787
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -8588,7 +8805,7 @@ async function runVerify(opts) {
8588
8805
  });
8589
8806
  }
8590
8807
  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"))
8808
+ (c) => !existsSync25(path31.join(task.set, c.rep, "get_screenshot.json")) || !existsSync25(path31.join(task.set, c.rep, "get_metadata.json"))
8592
8809
  );
8593
8810
  if (missing.length > 0) {
8594
8811
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -8598,10 +8815,10 @@ async function runVerify(opts) {
8598
8815
  });
8599
8816
  }
8600
8817
  const bar = BARS2[opts.bar];
8601
- const evidenceDir = path30.join(opts.bundleDir, "verify-evidence");
8818
+ const evidenceDir = path31.join(opts.bundleDir, "verify-evidence");
8602
8819
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
8603
8820
  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");
8821
+ 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
8822
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss);
8606
8823
  const parity = await checkHoverParity(task, opts.bundleDir);
8607
8824
  const behaviors = [...await checkBehaviors(task, opts.bundleDir), ...parity];
@@ -8818,18 +9035,18 @@ __export(engine_exports, {
8818
9035
  runEngineBrief: () => runEngineBrief,
8819
9036
  runEngineScore: () => runEngineScore
8820
9037
  });
8821
- import { existsSync as existsSync25, mkdirSync as mkdirSync6, readFileSync as readFileSync22, writeFileSync as writeFileSync11 } from "node:fs";
8822
- import path31 from "node:path";
9038
+ import { existsSync as existsSync26, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
9039
+ import path32 from "node:path";
8823
9040
  function resolveEngineTask(opts, callerCwd) {
8824
- const asPath = path31.resolve(callerCwd, opts.taskOrSet);
8825
- const isSet = existsSync25(path31.join(asPath, "recording-set.json"));
9041
+ const asPath = path32.resolve(callerCwd, opts.taskOrSet);
9042
+ const isSet = existsSync26(path32.join(asPath, "recording-set.json"));
8826
9043
  const registry = TASKS[opts.taskOrSet];
8827
9044
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet };
8828
9045
  if (isSet) {
8829
9046
  try {
8830
9047
  const authored = authorTaskFromSet(asPath);
8831
9048
  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 } };
9049
+ return { task: authored.task, name: path32.basename(asPath), apiPin: { props: authored.api.apiPin.props, forcedStates: authored.api.apiPin.forcedStates } };
8833
9050
  } catch (err) {
8834
9051
  fail(opts, ExitCode.InputValidation, {
8835
9052
  error: `cannot author a task from ${asPath}: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
@@ -8845,15 +9062,16 @@ function resolveEngineTask(opts, callerCwd) {
8845
9062
  });
8846
9063
  }
8847
9064
  function runEngineBrief(opts) {
9065
+ requireEntitlement(opts);
8848
9066
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8849
9067
  const { task, name } = resolveEngineTask(opts, callerCwd);
8850
9068
  const bar = BARS3[opts.bar];
8851
9069
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" });
8852
9070
  const segments = buildSegments(task, "files");
8853
9071
  let notRecorded;
8854
- const manifestPath2 = path31.join(task.set, "recording-set.json");
8855
- if (existsSync25(manifestPath2)) {
8856
- notRecorded = JSON.parse(readFileSync22(manifestPath2, "utf8")).notRecorded;
9072
+ const manifestPath2 = path32.join(task.set, "recording-set.json");
9073
+ if (existsSync26(manifestPath2)) {
9074
+ notRecorded = JSON.parse(readFileSync23(manifestPath2, "utf8")).notRecorded;
8857
9075
  }
8858
9076
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
8859
9077
 
@@ -8861,7 +9079,7 @@ function runEngineBrief(opts) {
8861
9079
  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
9080
  ${notRecorded}` : "";
8863
9081
  let fontProvisioning;
8864
- if (existsSync25(manifestPath2)) {
9082
+ if (existsSync26(manifestPath2)) {
8865
9083
  const provided = resolvedFontFamilies().map((f) => f.family);
8866
9084
  const unprovided = recordedFontFamilies(task.set).filter((r) => !provided.some((p) => p.toLowerCase() === r.toLowerCase()));
8867
9085
  if (unprovided.length > 0) {
@@ -8883,9 +9101,9 @@ ${notRecorded}` : "";
8883
9101
 
8884
9102
  === TASK PAYLOAD (recorded truth, verbatim) ===
8885
9103
  ${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);
9104
+ const payloadFile = path32.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
9105
+ mkdirSync7(path32.dirname(payloadFile), { recursive: true });
9106
+ writeFileSync12(payloadFile, payload);
8889
9107
  emitData(
8890
9108
  opts,
8891
9109
  {
@@ -8927,10 +9145,11 @@ ${segments}`;
8927
9145
  );
8928
9146
  }
8929
9147
  async function runEngineScore(opts) {
9148
+ requireEntitlement(opts);
8930
9149
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
8931
- const candidateDir = path31.resolve(callerCwd, opts.candidateDir);
9150
+ const candidateDir = path32.resolve(callerCwd, opts.candidateDir);
8932
9151
  const { task, name, apiPin } = resolveEngineTask(opts, callerCwd);
8933
- if (!existsSync25(candidateDir)) {
9152
+ if (!existsSync26(candidateDir)) {
8934
9153
  fail(opts, ExitCode.InputValidation, {
8935
9154
  error: `candidate directory not found: ${candidateDir}`,
8936
9155
  code: "candidate-missing",
@@ -8944,10 +9163,10 @@ async function runEngineScore(opts) {
8944
9163
  remediation: fontsUnprovenRemediation(task.set)
8945
9164
  });
8946
9165
  }
8947
- if (opts.rebind !== true && existsSync25(path31.join(candidateDir, "component.json"))) {
9166
+ if (opts.rebind !== true && existsSync26(path32.join(candidateDir, "component.json"))) {
8948
9167
  const prior = (() => {
8949
9168
  try {
8950
- const read = readBundleManifest(readFileSync22(path31.join(candidateDir, "component.json"), "utf8"));
9169
+ const read = readBundleManifest(readFileSync23(path32.join(candidateDir, "component.json"), "utf8"));
8951
9170
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
8952
9171
  } catch {
8953
9172
  return { unreadable: true };
@@ -8969,7 +9188,7 @@ async function runEngineScore(opts) {
8969
9188
  }
8970
9189
  }
8971
9190
  const bar = BARS3[opts.bar];
8972
- const evidenceDir = path31.join(candidateDir, "verify-evidence");
9191
+ const evidenceDir = path32.join(candidateDir, "verify-evidence");
8973
9192
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
8974
9193
  const parity = await checkHoverParity(task, candidateDir);
8975
9194
  const behaviors = [...await checkBehaviors(task, candidateDir), ...parity];
@@ -8984,11 +9203,16 @@ ${[
8984
9203
  ...quality.findings.map((f) => `- ${f.kind} ${f.file}${f.line === void 0 ? "" : `:${f.line}`} \u2014 ${f.message}`),
8985
9204
  ...quality.tokensAbsent ? ["- no design tokens: no tokens.css and no var(--\u2026) reference; every value is hardcoded"] : []
8986
9205
  ].join("\n")}`;
8987
- const feedback = buildFeedback(scores, behaviors, bar, "files") + qualityFeedback;
8988
9206
  const allPass = obj[0] === total && total > 0;
8989
9207
  const certBar = BARS3["cert"];
8990
- const certifiedReps = scores.filter((sc) => sc.similarity >= certBar.sim && sc.inkRecall >= certBar.ink).map((sc) => sc.rep);
9208
+ const parityDemoted = new Set(parity.filter((p) => !p.pass).map((p) => p.id.replace(/^parity:/, "")));
9209
+ const certifiedReps = scores.filter((sc) => tierOf(sc, certBar) === "certified" && !parityDemoted.has(sc.rep)).map((sc) => sc.rep);
8991
9210
  const certifiedSet = new Set(certifiedReps);
9211
+ const certificationFeedback = `
9212
+
9213
+ 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(", ")}.` : ""}
9214
+ 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.`;
9215
+ const feedback = buildFeedback(scores, behaviors, bar, "files") + certificationFeedback + qualityFeedback;
8992
9216
  const emitted = emitBundleV1({
8993
9217
  bundleDir: candidateDir,
8994
9218
  task,
@@ -9019,7 +9243,7 @@ ${[
9019
9243
  bundleManifest: emitted.written[0],
9020
9244
  note: "styles.css was stamped with the bundle provenance comment on line 1 \u2014 preserve it in any post-score edit",
9021
9245
  allPass,
9022
- certification: { certified: certifiedReps.length, total, bar: certBar }
9246
+ certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity demotion; verify's composition checks can demote further" }
9023
9247
  },
9024
9248
  () => {
9025
9249
  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 +9273,9 @@ var init_engine2 = __esm({
9049
9273
  init_font_guidance();
9050
9274
  init_src4();
9051
9275
  init_output();
9276
+ init_entitlement();
9052
9277
  init_verify();
9278
+ init_src4();
9053
9279
  BARS3 = {
9054
9280
  pass: { sim: 0.95, ink: 0.95 },
9055
9281
  cert: { sim: 0.97, ink: 0.95 }
@@ -9062,11 +9288,11 @@ var codeconnect_exports = {};
9062
9288
  __export(codeconnect_exports, {
9063
9289
  runCodeConnect: () => runCodeConnect
9064
9290
  });
9065
- import { existsSync as existsSync26, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "node:fs";
9066
- import path32 from "node:path";
9291
+ import { existsSync as existsSync27, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "node:fs";
9292
+ import path33 from "node:path";
9067
9293
  function runCodeConnect(opts) {
9068
9294
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9069
- const bundleDir = path32.resolve(callerCwd, opts.bundleDir);
9295
+ const bundleDir = path33.resolve(callerCwd, opts.bundleDir);
9070
9296
  let url;
9071
9297
  try {
9072
9298
  url = new URL(opts.figmaUrl);
@@ -9082,7 +9308,7 @@ function runCodeConnect(opts) {
9082
9308
  }
9083
9309
  let manifest;
9084
9310
  try {
9085
- const read = readBundleManifest(readFileSync23(path32.join(bundleDir, "component.json"), "utf8"));
9311
+ const read = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8"));
9086
9312
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
9087
9313
  manifest = read.manifest;
9088
9314
  } catch (err) {
@@ -9092,8 +9318,8 @@ function runCodeConnect(opts) {
9092
9318
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
9093
9319
  });
9094
9320
  }
9095
- const setDir = path32.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
9096
- if (!existsSync26(path32.join(setDir, "recording-set.json"))) {
9321
+ const setDir = path33.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
9322
+ if (!existsSync27(path33.join(setDir, "recording-set.json"))) {
9097
9323
  fail(opts, ExitCode.InputValidation, {
9098
9324
  error: `recording set not found at ${setDir}`,
9099
9325
  code: "codeconnect-no-set",
@@ -9114,10 +9340,10 @@ function runCodeConnect(opts) {
9114
9340
  const component = api.component;
9115
9341
  const recManifest = loadManifest(setDir);
9116
9342
  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;
9343
+ const meta = path33.join(setDir, r.slug, "get_metadata.json");
9344
+ if (!existsSync27(meta)) return void 0;
9119
9345
  try {
9120
- return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync23(meta, "utf8"))))?.[1];
9346
+ return /name="([^"]*)"/.exec(envelopeFirstTextPart(JSON.parse(readFileSync24(meta, "utf8"))))?.[1];
9121
9347
  } catch {
9122
9348
  return void 0;
9123
9349
  }
@@ -9175,7 +9401,7 @@ function runCodeConnect(opts) {
9175
9401
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
9176
9402
  fragmentVars.push(varName);
9177
9403
  }
9178
- const entryRel = path32.relative(callerCwd, path32.join(bundleDir, manifest.entry));
9404
+ const entryRel = path33.relative(callerCwd, path33.join(bundleDir, manifest.entry));
9179
9405
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
9180
9406
  const lines = [
9181
9407
  `// url=${opts.figmaUrl}`,
@@ -9196,8 +9422,8 @@ function runCodeConnect(opts) {
9196
9422
  `}`,
9197
9423
  ``
9198
9424
  ].join("\n");
9199
- const outFile = path32.resolve(callerCwd, opts.out ?? path32.join(bundleDir, `${component}.figma.ts`));
9200
- writeFileSync12(outFile, lines);
9425
+ const outFile = path33.resolve(callerCwd, opts.out ?? path33.join(bundleDir, `${component}.figma.ts`));
9426
+ writeFileSync13(outFile, lines);
9201
9427
  emitData(
9202
9428
  opts,
9203
9429
  {
@@ -9257,17 +9483,17 @@ __export(generate_recorded_exports, {
9257
9483
  runGenerateRecorded: () => runGenerateRecorded
9258
9484
  });
9259
9485
  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";
9486
+ import { existsSync as existsSync28, readFileSync as readFileSync25 } from "node:fs";
9487
+ import path34 from "node:path";
9262
9488
  async function runGenerateRecorded(opts) {
9263
9489
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
9264
- const outDirAbs = path33.resolve(callerCwd, opts.out);
9265
- const recordedAsPath = path33.resolve(callerCwd, opts.recorded);
9490
+ const outDirAbs = path34.resolve(callerCwd, opts.out);
9491
+ const recordedAsPath = path34.resolve(callerCwd, opts.recorded);
9266
9492
  let task;
9267
9493
  let taskName;
9268
9494
  let authoredApi;
9269
9495
  let composition;
9270
- const isSet = existsSync27(path33.join(recordedAsPath, "recording-set.json"));
9496
+ const isSet = existsSync28(path34.join(recordedAsPath, "recording-set.json"));
9271
9497
  const registry = TASKS[opts.recorded];
9272
9498
  if (registry !== void 0 && !isSet) {
9273
9499
  task = registry;
@@ -9276,7 +9502,7 @@ async function runGenerateRecorded(opts) {
9276
9502
  try {
9277
9503
  const authored = authorTaskFromSet(recordedAsPath);
9278
9504
  task = authored.task;
9279
- taskName = path33.basename(recordedAsPath);
9505
+ taskName = path34.basename(recordedAsPath);
9280
9506
  authoredApi = authored.api;
9281
9507
  const roles = RolesSchema.safeParse(loadManifest(recordedAsPath).roles);
9282
9508
  if (roles.success) composition = roles.data;
@@ -9303,7 +9529,7 @@ async function runGenerateRecorded(opts) {
9303
9529
  });
9304
9530
  }
9305
9531
  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"))
9532
+ (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
9533
  );
9308
9534
  if (missing.length > 0) {
9309
9535
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -9373,8 +9599,8 @@ async function runGenerateRecorded(opts) {
9373
9599
  ` : `${line}
9374
9600
  `);
9375
9601
  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)})
9602
+ emitData(opts, { dryRun: true, task: taskName, model: modelId, consent, wouldWrite: path34.join(outDirAbs, taskName) }, () => {
9603
+ process.stdout.write(`dry-run: nothing sent, nothing written (would write ${path34.join(outDirAbs, taskName)})
9378
9604
  `);
9379
9605
  });
9380
9606
  return;
@@ -9397,10 +9623,10 @@ async function runGenerateRecorded(opts) {
9397
9623
  });
9398
9624
  }
9399
9625
  }
9400
- const bundleDir = path33.join(outDirAbs, taskName);
9401
- if (existsSync27(path33.join(bundleDir, "component.json"))) {
9626
+ const bundleDir = path34.join(outDirAbs, taskName);
9627
+ if (existsSync28(path34.join(bundleDir, "component.json"))) {
9402
9628
  try {
9403
- const prior = readBundleManifest(readFileSync24(path33.join(bundleDir, "component.json"), "utf8")).manifest;
9629
+ const prior = readBundleManifest(readFileSync25(path34.join(bundleDir, "component.json"), "utf8")).manifest;
9404
9630
  if (prior !== void 0 && prior.provenance.recordingSet.hash !== recordingSetHash(task.set, task.configs)) {
9405
9631
  fail(opts, ExitCode.InputValidation, {
9406
9632
  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 +9762,9 @@ import { Command } from "commander";
9536
9762
  // packages/cli/src/commands/doctor.ts
9537
9763
  init_src4();
9538
9764
  init_src();
9539
- import { existsSync as existsSync16, readFileSync as readFileSync13 } from "node:fs";
9540
- import path21 from "node:path";
9765
+ import { existsSync as existsSync17, readFileSync as readFileSync14, readdirSync as readdirSync3 } from "node:fs";
9766
+ import os4 from "node:os";
9767
+ import path22 from "node:path";
9541
9768
 
9542
9769
  // packages/cli/src/describe.ts
9543
9770
  var COMMON_EXIT_CODES = {
@@ -9556,6 +9783,7 @@ function printDescription(description) {
9556
9783
  init_env();
9557
9784
  init_environment();
9558
9785
  init_output();
9786
+ init_entitlement();
9559
9787
  var DEFAULT_MCP_URL = "http://127.0.0.1:3845/mcp";
9560
9788
  var DOCTOR_DESCRIPTION = {
9561
9789
  name: "doctor",
@@ -9615,15 +9843,38 @@ async function runDoctorChecks(options) {
9615
9843
  remediation: "Install Google Chrome (or Chromium), or set CHROME_PATH to a Chromium-family browser binary."
9616
9844
  });
9617
9845
  }
9618
- const fontManifest = path21.join(fontCacheDir(), "manifest.json");
9846
+ const fontManifest = path22.join(fontCacheDir(), "manifest.json");
9619
9847
  checks.push(
9620
- existsSync16(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync13(fontManifest, "utf8")).length} faces)` } : {
9848
+ existsSync17(fontManifest) ? { name: "font-cache", ok: true, detail: `resolved font cache at ${fontCacheDir()} (${JSON.parse(readFileSync14(fontManifest, "utf8")).length} faces)` } : {
9621
9849
  name: "font-cache",
9622
9850
  ok: true,
9623
9851
  detail: `font cache empty at ${fontCacheDir()} \u2014 normal on a fresh machine; faces resolve per design system at first use`,
9624
9852
  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
9853
  }
9626
9854
  );
9855
+ const pluginRoot = path22.join(os4.homedir(), ".claude", "plugins", "cache", "tendrilapp", "tendril");
9856
+ if (existsSync17(pluginRoot)) {
9857
+ try {
9858
+ const versions = readdirSync3(pluginRoot).filter((v) => /^\d+\.\d+\.\d+$/.test(v));
9859
+ const newest = versions.sort((a, b) => versionIsNewer(a, b) ? 1 : -1)[0];
9860
+ if (newest !== void 0) {
9861
+ const skewed = versionIsNewer(newest, cliVersion());
9862
+ checks.push(
9863
+ skewed ? {
9864
+ name: "plugin-skew",
9865
+ ok: false,
9866
+ 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`,
9867
+ remediation: "Update the plugin: terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel: uninstall tendril, reinstall, reopen the chat panel."
9868
+ } : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
9869
+ );
9870
+ }
9871
+ } catch {
9872
+ }
9873
+ }
9874
+ const ent = checkEntitlement();
9875
+ checks.push(
9876
+ 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 }
9877
+ );
9627
9878
  const figmaToken = resolveCredential("FIGMA_TOKEN");
9628
9879
  checks.push({
9629
9880
  name: "figma-pat",
@@ -9692,7 +9943,7 @@ async function runDoctor(flags) {
9692
9943
  init_src3();
9693
9944
  import { intro, isCancel, outro, password } from "@clack/prompts";
9694
9945
  import fs from "node:fs";
9695
- import path22 from "node:path";
9946
+ import path23 from "node:path";
9696
9947
  init_env();
9697
9948
  init_output();
9698
9949
  var INIT_DESCRIPTION = {
@@ -9731,7 +9982,7 @@ async function runInit(flags) {
9731
9982
  printDescription(INIT_DESCRIPTION);
9732
9983
  return;
9733
9984
  }
9734
- const envPath = path22.resolve(process.cwd(), ".env");
9985
+ const envPath = path23.resolve(process.cwd(), ".env");
9735
9986
  const existing = fs.existsSync(envPath) ? parseEnv(fs.readFileSync(envPath, "utf8")) : /* @__PURE__ */ new Map();
9736
9987
  let figmaToken = flags.figmaToken ?? existing.get(ENV_KEYS.figma);
9737
9988
  let openrouterKey = flags.openrouterKey ?? existing.get(ENV_KEYS.openrouter);
@@ -9752,7 +10003,7 @@ async function runInit(flags) {
9752
10003
  next.set(ENV_KEYS.figma, figmaToken);
9753
10004
  next.set(ENV_KEYS.openrouter, openrouterKey);
9754
10005
  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");
10006
+ const gitignorePath = path23.resolve(process.cwd(), ".gitignore");
9756
10007
  const gitignore = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
9757
10008
  const gitignoreCoversEnv = gitignore.split("\n").some((line) => [".env", ".env", ".env*"].includes(line.trim()));
9758
10009
  if (flags.dryRun) {
@@ -9803,16 +10054,17 @@ init_src();
9803
10054
  init_src5();
9804
10055
  init_src2();
9805
10056
  import { confirm as confirm2, isCancel as isCancel2 } from "@clack/prompts";
9806
- import { readFileSync as readFileSync14, readdirSync as readdirSync3, existsSync as existsSync17 } from "node:fs";
10057
+ import { readFileSync as readFileSync15, readdirSync as readdirSync4, existsSync as existsSync18 } from "node:fs";
9807
10058
  init_env();
9808
10059
  init_output();
10060
+ init_entitlement();
9809
10061
 
9810
10062
  // packages/cli/src/pipeline.ts
9811
10063
  init_src2();
9812
10064
  init_src4();
9813
10065
  init_src6();
9814
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync7 } from "node:fs";
9815
- import path23 from "node:path";
10066
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
10067
+ import path24 from "node:path";
9816
10068
 
9817
10069
  // packages/cli/src/assets-module.ts
9818
10070
  init_src();
@@ -10148,8 +10400,8 @@ async function runGenerationPipeline(input) {
10148
10400
  });
10149
10401
  const written = [];
10150
10402
  if (!input.dryRun) {
10151
- const dir = path23.resolve(input.outDir, semantics.componentName);
10152
- mkdirSync4(dir, { recursive: true });
10403
+ const dir = path24.resolve(input.outDir, semantics.componentName);
10404
+ mkdirSync5(dir, { recursive: true });
10153
10405
  const files = {
10154
10406
  // Bundle-local tokens: THE emission the component's CSS resolves
10155
10407
  // against — the same artifact the verify harness injects. A preview
@@ -10172,14 +10424,14 @@ async function runGenerationPipeline(input) {
10172
10424
  `
10173
10425
  };
10174
10426
  for (const [name, content] of Object.entries(files)) {
10175
- const filePath = path23.join(dir, name);
10176
- writeFileSync7(filePath, content);
10427
+ const filePath = path24.join(dir, name);
10428
+ writeFileSync8(filePath, content);
10177
10429
  written.push(filePath);
10178
10430
  }
10179
10431
  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);
10432
+ const filePath = path24.resolve(input.outDir, artifact.path);
10433
+ mkdirSync5(path24.dirname(filePath), { recursive: true });
10434
+ writeFileSync8(filePath, artifact.content);
10183
10435
  written.push(filePath);
10184
10436
  }
10185
10437
  }
@@ -10237,7 +10489,7 @@ var GENERATE_DESCRIPTION = {
10237
10489
  function resolveProvidedSource(flags, contextFile) {
10238
10490
  let raw;
10239
10491
  try {
10240
- raw = readFileSync14(contextFile, "utf8");
10492
+ raw = readFileSync15(contextFile, "utf8");
10241
10493
  } catch {
10242
10494
  fail(flags, ExitCode.InputValidation, {
10243
10495
  error: `Cannot read context file "${contextFile}".`,
@@ -10271,6 +10523,7 @@ function resolveSource(flags, url) {
10271
10523
  });
10272
10524
  }
10273
10525
  async function runGenerate(url, flags) {
10526
+ requireEntitlement(flags);
10274
10527
  if (flags.describe) {
10275
10528
  printDescription(GENERATE_DESCRIPTION);
10276
10529
  return;
@@ -10356,11 +10609,11 @@ token mapping (${mapping.flat.length} variables):
10356
10609
  let initialCode;
10357
10610
  let initialSemantics;
10358
10611
  try {
10359
- if (existsSync17(flags.out)) {
10360
- for (const entry of readdirSync3(flags.out)) {
10612
+ if (existsSync18(flags.out)) {
10613
+ for (const entry of readdirSync4(flags.out)) {
10361
10614
  const cjPath = `${flags.out}/${entry}/component.json`;
10362
- if (!existsSync17(cjPath)) continue;
10363
- const cj = JSON.parse(readFileSync14(cjPath, "utf8"));
10615
+ if (!existsSync18(cjPath)) continue;
10616
+ const cj = JSON.parse(readFileSync15(cjPath, "utf8"));
10364
10617
  if (cj.name !== void 0 && Array.isArray(cj.props)) {
10365
10618
  previousApi = JSON.stringify({
10366
10619
  componentName: cj.name,
@@ -10368,14 +10621,14 @@ token mapping (${mapping.flat.length} variables):
10368
10621
  });
10369
10622
  const tsxPath = `${flags.out}/${entry}/${cj.name}.tsx`;
10370
10623
  const cssPath = `${flags.out}/${entry}/${cj.name}.css`;
10371
- if (flags.refine && existsSync17(tsxPath) && existsSync17(cssPath)) {
10624
+ if (flags.refine && existsSync18(tsxPath) && existsSync18(cssPath)) {
10372
10625
  initialCode = {
10373
- tsx: readFileSync14(tsxPath, "utf8"),
10374
- css: readFileSync14(cssPath, "utf8")
10626
+ tsx: readFileSync15(tsxPath, "utf8"),
10627
+ css: readFileSync15(cssPath, "utf8")
10375
10628
  };
10376
10629
  const semPath = `${flags.out}/${entry}/semantics.json`;
10377
- if (existsSync17(semPath)) {
10378
- initialSemantics = JSON.parse(readFileSync14(semPath, "utf8"));
10630
+ if (existsSync18(semPath)) {
10631
+ initialSemantics = JSON.parse(readFileSync15(semPath, "utf8"));
10379
10632
  }
10380
10633
  }
10381
10634
  break;
@@ -10544,6 +10797,12 @@ function buildProgram() {
10544
10797
  const local = cmd.opts();
10545
10798
  await runDoctor({ ...flags, mcpUrl: local["mcpUrl"] });
10546
10799
  });
10800
+ 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) => {
10801
+ const flags = globalFlags(cmd);
10802
+ const local = cmd.opts();
10803
+ const { runActivate: runActivate2 } = await Promise.resolve().then(() => (init_activate(), activate_exports));
10804
+ await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
10805
+ });
10547
10806
  const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
10548
10807
  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
10808
  const flags = globalFlags(cmd.parent.parent);