@tendrilapp/cli 0.1.29 → 0.1.31

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/SKILL.md CHANGED
@@ -131,7 +131,12 @@ Batch runs (several components in one session):
131
131
  2. `tendril_record_plan` with the response text passed VERBATIM via
132
132
  `metadataParts` (one array entry per response block, in order —
133
133
  single-block responses may use `metadata` instead; nothing is saved
134
- to a file, and never hand-join blocks) the queue plan. The plan
134
+ to a file, and never hand-join blocks), AND `figmaFile` set to the
135
+ file key from the URL you were handed (figma.com/design/<KEY>/… —
136
+ pass exactly KEY): it is recorded as the set's file identity for
137
+ cross-bundle composition, captured at plan time only and never
138
+ backfillable — a set planned without it can never join across
139
+ bundles → the queue plan. The plan
135
140
  records the FULL variant matrix — every pose the set defines
136
141
  (sampling exists only as a human-run CLI flag; the MCP surface
137
142
  cannot sample). CROSS-CHECK `variantsFound` in the plan output
@@ -254,8 +259,13 @@ Batch runs (several components in one session):
254
259
  CSS. Import every React API you use.
255
260
  3. `tendril_engine_score` → read the per-config results and feedback.
256
261
  Fix FAIL configs without regressing PASS configs; re-score.
257
- 4. Stop when `allPass` is true, or after two consecutive rounds with
258
- no improvement then report the honest final state.
262
+ 4. Stop when `allPass` is true. Two consecutive rounds with no
263
+ improvement is a TRIGGER TO INSPECT, not to stop: open the
264
+ verify-evidence diff images for the worst configs and look before
265
+ deciding (a measured run scored byte-identical twice, inspected
266
+ the diffs, and round 3 went 27/27 certified — a stop rule would
267
+ have shipped 5/27). Stop only after an inspection round produced
268
+ no fix hypothesis — then report the honest final state.
259
269
  5. MODEL SELECTION — mechanical AND asked. `engine brief` and
260
270
  `engine score` refuse to run without a declared model, so the
261
271
  choice must be settled first. When a user is present, ask before
@@ -37,7 +37,8 @@ var TOOLS = [
37
37
  metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
38
38
  metadataFiles: z.array(z.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
39
39
  defaults: z.array(z.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
40
- componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
40
+ componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)"),
41
+ figmaFile: optStr("the Figma file key from the design URL you were handed \u2014 figma.com/design/<KEY>/\u2026, pass exactly <KEY>. ALWAYS pass it on a fresh plan: it is the set's recorded file identity for cross-bundle composition (ADR-013), captured at plan time only and never backfillable later")
41
42
  }),
42
43
  // Texts ride temp files, never argv: Windows caps a command line at
43
44
  // ~32 KB and metadata envelopes can exceed it.
@@ -60,7 +61,11 @@ var TOOLS = [
60
61
  }
61
62
  }
62
63
  if (argvOut.length === 6) throw new Error("nothing to plan from \u2014 pass metadataParts (normal), metadata (single-block), or metadataFiles");
63
- argvOut.push(...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [], ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []);
64
+ argvOut.push(
65
+ ...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [],
66
+ ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : [],
67
+ ...typeof i["figmaFile"] === "string" ? ["--figma-file", i["figmaFile"]] : []
68
+ );
64
69
  return argvOut;
65
70
  }
66
71
  },
package/dist/tendril.js CHANGED
@@ -1069,6 +1069,9 @@ function planSet(setDir, component, symbols, opts = {}) {
1069
1069
  const manifest = {
1070
1070
  version: 1,
1071
1071
  component,
1072
+ ...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
1073
+ ...opts.componentSetNode !== void 0 ? { componentSetNode: opts.componentSetNode } : {},
1074
+ ...opts.figmaComponentName !== void 0 ? { figmaComponentName: opts.figmaComponentName } : {},
1072
1075
  ...opts.sourceFrames !== void 0 ? { sourceFrames: opts.sourceFrames } : {},
1073
1076
  ...opts.variantScope !== void 0 ? { variantScope: opts.variantScope } : {},
1074
1077
  ...opts.defaults !== void 0 && Object.keys(opts.defaults).length > 0 ? { defaults: opts.defaults } : {},
@@ -1215,8 +1218,27 @@ var init_session = __esm({
1215
1218
  SessionManifestSchema = z4.object({
1216
1219
  version: z4.literal(1),
1217
1220
  component: z4.string().min(1),
1221
+ /** ADR-013 slice 1 (v4, capture-only): cross-set identity, captured
1222
+ * at RECORD TIME on fresh plans only — NEVER backfilled (the
1223
+ * recording-set hash covers this file's bytes, so adding fields to
1224
+ * a set that already produced bundles voids their provenance).
1225
+ * `figmaFile` is the Figma file key from the URL the agent host was
1226
+ * handed — an AGENT ASSERTION, disclosed, never enforcement-bearing
1227
+ * alone (ADR-013 §1). `componentSetNode`/`figmaComponentName` are
1228
+ * the enclosing set's id and Figma-side name as the planner's own
1229
+ * metadata parse saw them — the identity that stops a re-recording
1230
+ * of the same component from reading as a different one (`component`
1231
+ * below is the plan-time DISPLAY name; the measured case recorded
1232
+ * one control as "Lucent Control (Dark)" and "control"). All
1233
+ * optional: legacy sets simply cannot join, disclosed. */
1234
+ figmaFile: z4.string().regex(/^[A-Za-z0-9]{8,64}$/).optional(),
1235
+ componentSetNode: z4.string().optional(),
1236
+ figmaComponentName: z4.string().optional(),
1218
1237
  sourceFrames: z4.record(z4.string(), z4.string()).optional(),
1219
- reps: z4.array(z4.object({ slug: z4.string(), nodeId: z4.string(), sourceFrame: z4.string().optional() })).min(1),
1238
+ /** Per-rep `componentKey` is SEEDED (ADR-013 v4: the one-call MCP
1239
+ * probe decides whether anything can ever fill it) so interim sets
1240
+ * are not born cross-file-dead if keys turn out to be available. */
1241
+ reps: z4.array(z4.object({ slug: z4.string(), nodeId: z4.string(), sourceFrame: z4.string().optional(), componentKey: z4.string().optional() })).min(1),
1220
1242
  notRecorded: z4.string().optional(),
1221
1243
  /** Explicit axis-default overrides from `record plan --default`.
1222
1244
  * Recorded-truth-side on purpose: the default decides which pose an
@@ -2232,6 +2254,8 @@ async function runTokenLint(css, fileLabel = "generated.css", definedVars2, reco
2232
2254
  property: "custom-property-ident",
2233
2255
  message: `var(${name}) is not a legal custom property reference \u2014 the browser drops the whole declaration`
2234
2256
  });
2257
+ } else if (name.startsWith("--tendril-")) {
2258
+ continue;
2235
2259
  } else if (definedVars2 !== void 0 && !definedVars2.has(name) && !ownDefs.has(name)) {
2236
2260
  violations.push({
2237
2261
  file: fileLabel,
@@ -7871,10 +7895,10 @@ function symbolsFromMetadataEnvelope(file, sourceFrame) {
7871
7895
  nodeId: node.id,
7872
7896
  name: node.name,
7873
7897
  ...sourceFrame !== void 0 ? { sourceFrame } : {},
7874
- ...ancestor !== void 0 ? { setName: ancestor } : {}
7898
+ ...ancestor !== void 0 ? { setName: ancestor.name, setNodeId: ancestor.id } : {}
7875
7899
  });
7876
7900
  }
7877
- const next = node.type !== "COMPONENT" && node.name !== "" ? node.name : ancestor;
7901
+ const next = node.type !== "COMPONENT" && node.name !== "" ? { name: node.name, id: node.id } : ancestor;
7878
7902
  for (const child of node.children) walk2(child, next);
7879
7903
  };
7880
7904
  const forest = parseMetadataForest(text);
@@ -8012,10 +8036,25 @@ function runRecordPlan(opts) {
8012
8036
  if (metadataTruncated) {
8013
8037
  warn(opts, "get_metadata response appears TRUNCATED (unclosed structure) \u2014 the variant list below may be incomplete. Cross-check variantsFound against the variant count Figma shows for this component set; re-fetch the metadata if lower.");
8014
8038
  }
8039
+ if (opts.figmaFile !== void 0 && !/^[A-Za-z0-9]{8,64}$/.test(opts.figmaFile)) {
8040
+ fail(opts, ExitCode.InputValidation, {
8041
+ error: `--figma-file must be the bare file key from the Figma URL (letters and digits only) \u2014 got ${JSON.stringify(opts.figmaFile)}`,
8042
+ code: "bad-figma-file",
8043
+ remediation: "From https://www.figma.com/design/<KEY>/<name>?node-id=\u2026, pass exactly <KEY>."
8044
+ });
8045
+ }
8046
+ const setIdentity = (() => {
8047
+ if (variantScope !== "component-set") return {};
8048
+ const ids = [...new Set(symbols.map((s) => s.setNodeId).filter((x) => x !== void 0))];
8049
+ const names = [...new Set(symbols.map((s) => s.setName).filter((x) => x !== void 0))];
8050
+ return ids.length === 1 && names.length === 1 ? { componentSetNode: ids[0], figmaComponentName: names[0] } : {};
8051
+ })();
8015
8052
  const { manifest, plan, resumed, toppedUp } = planSet(opts.setDir, opts.component, symbols, {
8016
8053
  ...Object.keys(defaults).length > 0 ? { defaults } : {},
8017
8054
  ...opts.sample === true ? { sample: true } : {},
8018
- variantScope
8055
+ variantScope,
8056
+ ...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
8057
+ ...setIdentity
8019
8058
  });
8020
8059
  const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
8021
8060
  const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
@@ -11146,14 +11185,15 @@ function latticeCoverage(setManifest, scoredConfigs) {
11146
11185
  const lattice = setManifest.latticeNames?.length;
11147
11186
  const established = setManifest.variantScope === "component-set";
11148
11187
  const notRecorded = setManifest.notRecorded !== void 0 && setManifest.notRecorded !== "" ? { notRecorded: setManifest.notRecorded } : {};
11149
- if (lattice === void 0) return { latticeConfigs: null, ...notRecorded };
11188
+ const latticeUnestablished = setManifest.variantScope === "selection" ? "the recording was planned from a node SELECTION, not from a component set \u2014 the plan counted the nodes it was handed and could not see whether the set holds more variants, so the number of unrecorded poses is UNKNOWN, not zero" : setManifest.variantScope === "component-set" ? "the planner saw the component set and it declares no variant axes \u2014 the planned pose(s) are the whole set as Figma showed it, but there is no variant lattice to count" : "this set was planned before variant scope was recorded, so whether its lattice came from a component set or from a handed-in selection was never established \u2014 the number of unrecorded poses is unknown, not zero";
11189
+ if (lattice === void 0) return { latticeConfigs: null, latticeUnestablished, ...notRecorded };
11150
11190
  if (established) return { latticeConfigs: lattice, unrecordedConfigs: Math.max(0, lattice - scoredConfigs), ...notRecorded };
11151
11191
  return {
11152
11192
  latticeConfigs: null,
11153
11193
  // The count is still worth reporting — it is just not a
11154
11194
  // denominator, and the name has to stop implying that it is.
11155
11195
  variantsPlanned: lattice,
11156
- latticeUnestablished: setManifest.variantScope === "selection" ? "the recording was planned from a node SELECTION, not from a component set \u2014 the plan counted the nodes it was handed and could not see whether the set holds more variants, so the number of unrecorded poses is UNKNOWN, not zero" : "this set was planned before variant scope was recorded, so whether its lattice came from a component set or from a handed-in selection was never established \u2014 the number of unrecorded poses is unknown, not zero",
11196
+ latticeUnestablished,
11157
11197
  ...notRecorded
11158
11198
  };
11159
11199
  }
@@ -11557,8 +11597,10 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
11557
11597
  process.stdout.write(`LATTICE all ${cov.latticeConfigs} poses of the component set's own lattice are recorded and scored \u2014 the denominator is the set's, not the recorded subset's
11558
11598
  `);
11559
11599
  } else if (cov.latticeConfigs === null) {
11560
- process.stdout.write(`COVERAGE denominator unknown (set predates lattice tracking) \u2014 scored configs are verified; completeness is not established
11561
- `);
11600
+ process.stdout.write(
11601
+ `COVERAGE denominator unknown \u2014 ${cov.latticeUnestablished ?? "set predates lattice tracking"} \u2014 scored configs are verified; completeness is not established
11602
+ `
11603
+ );
11562
11604
  }
11563
11605
  }
11564
11606
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
@@ -11767,7 +11809,7 @@ ${segments}`;
11767
11809
  `Run \`${tendrilCommand(
11768
11810
  `engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"} --json`
11769
11811
  )}\` \u2014 the CLI is the sole judge, and it refuses to score an undeclared model. If you wrote the bundle elsewhere, pass that directory instead.`,
11770
- "Apply the returned feedback and re-score. Stop when all checks pass or two consecutive scores fail to improve.",
11812
+ "Apply the returned feedback and re-score. Stop when all checks pass. Two consecutive non-improving scores mean INSPECT the verify-evidence diffs before deciding \u2014 stop only when inspection yields no fix hypothesis (a measured run was byte-identical twice, then went 27/27 after reading the diffs).",
11771
11813
  "Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
11772
11814
  ]
11773
11815
  },
@@ -12225,7 +12267,8 @@ var init_server = __esm({
12225
12267
  metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
12226
12268
  metadataFiles: z12.array(z12.string()).optional().describe('saved verbatim get_metadata envelope file paths \u2014 JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; optionally <file>@<frameId>. Prefer metadataParts: no file to write'),
12227
12269
  defaults: z12.array(z12.string()).optional().describe(`axis defaults as "Axis=Value" (from the user's defaultsToConfirm answers; may re-plan a set with nothing recorded yet)`),
12228
- componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
12270
+ componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)"),
12271
+ figmaFile: optStr("the Figma file key from the design URL you were handed \u2014 figma.com/design/<KEY>/\u2026, pass exactly <KEY>. ALWAYS pass it on a fresh plan: it is the set's recorded file identity for cross-bundle composition (ADR-013), captured at plan time only and never backfillable later")
12229
12272
  }),
12230
12273
  // Texts ride temp files, never argv: Windows caps a command line at
12231
12274
  // ~32 KB and metadata envelopes can exceed it.
@@ -12248,7 +12291,11 @@ var init_server = __esm({
12248
12291
  }
12249
12292
  }
12250
12293
  if (argvOut.length === 6) throw new Error("nothing to plan from \u2014 pass metadataParts (normal), metadata (single-block), or metadataFiles");
12251
- argvOut.push(...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [], ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []);
12294
+ argvOut.push(
12295
+ ...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [],
12296
+ ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : [],
12297
+ ...typeof i["figmaFile"] === "string" ? ["--figma-file", i["figmaFile"]] : []
12298
+ );
12252
12299
  return argvOut;
12253
12300
  }
12254
12301
  },
@@ -14068,7 +14115,7 @@ function buildProgram() {
14068
14115
  await runActivate2({ ...flags, serviceUrl: local["serviceUrl"] });
14069
14116
  });
14070
14117
  const record = program.command("record").description("Agent-driven recording protocol: plan the queue, get instructions, ingest verbatim envelopes, resume from disk.");
14071
- record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").option("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--metadata-raw-file <file>", "the get_metadata response TEXT exactly as received (single block); the CLI builds the envelope itself").option("--metadata-raw-parts-file <file>", "a JSON array of the get_metadata response blocks, each verbatim, in order (multi-block responses)").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) => {
14118
+ record.command("plan").requiredOption("--set <dir>", "recording set directory").requiredOption("--component <name>", "component/system name").option("--metadata <file...>", "verbatim get_metadata envelope(s), optionally <file>@<frameId>").option("--metadata-raw-file <file>", "the get_metadata response TEXT exactly as received (single block); the CLI builds the envelope itself").option("--metadata-raw-parts-file <file>", "a JSON array of the get_metadata response blocks, each verbatim, in order (multi-block responses)").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)").option("--figma-file <key>", "the Figma file key from the design URL (figma.com/design/<KEY>/\u2026) \u2014 recorded as the set's file identity for cross-bundle composition (ADR-013; fresh plans only, never backfilled)").action(async (_o, cmd) => {
14072
14119
  const flags = globalFlags(cmd.parent.parent);
14073
14120
  const local = cmd.opts();
14074
14121
  const { runRecordPlan: runRecordPlan2 } = await Promise.resolve().then(() => (init_record(), record_exports));
@@ -14077,6 +14124,7 @@ function buildProgram() {
14077
14124
  setDir: local["set"],
14078
14125
  component: local["component"],
14079
14126
  sample: local["sample"],
14127
+ ...local["figmaFile"] !== void 0 ? { figmaFile: local["figmaFile"] } : {},
14080
14128
  ...local["metadata"] !== void 0 ? { metadataFiles: local["metadata"] } : {},
14081
14129
  ...local["metadataRawFile"] !== void 0 ? { metadataRawFile: local["metadataRawFile"] } : {},
14082
14130
  ...local["metadataRawPartsFile"] !== void 0 ? { metadataRawPartsFile: local["metadataRawPartsFile"] } : {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",