@tendrilapp/cli 0.1.15 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/SKILL.md CHANGED
@@ -164,11 +164,24 @@ Non-negotiables (the CLI enforces these; do not fight them):
164
164
  6. Tell the user roughly what a run costs them: organism-scale
165
165
  components have measured 0.3–0.7M tokens of their plan.
166
166
 
167
- Delegated generation is SLOW BY NATURE — measured runs take 3–16
168
- minutes, and the one fully successful generator run did not write its
169
- first file until 14m24s. Do not treat silence as failure. Tell the
170
- user up front that a generator agent typically runs 5–15 minutes, and
171
- poll the candidate directory rather than guessing.
167
+ Delegated generation is SLOW BY NATURE — measured runs span 3–23
168
+ minutes (run 8: two healthy runs at ~23m each). Do not treat silence
169
+ as failure. Tell the user up front that a generator agent typically
170
+ runs 10–25 minutes, and poll the candidate directory rather than
171
+ guessing.
172
+
173
+ ESCALATION (when the balanced model plateaus below the bar): ask the
174
+ user before spending the premium tier; point the second generator at
175
+ the SAME candidate directory (never a fresh start — the work is
176
+ sunk); hand it a calibration table of failing vs passing configs and
177
+ the exact fields; never pass rebind. TRAP (measured, run 8): the
178
+ nearest PASSING config is not a safe regression tripwire — it may
179
+ carry the same defect and pass by luck (one config passed its bar by
180
+ 0.00089 while carrying the identical corner defect as the three
181
+ failures). An edit whose exact fields do not move across a round did
182
+ NOTHING: revert it before the next round — bundles must never
183
+ accumulate unsupported edits a later reader mistakes for recorded
184
+ truth.
172
185
 
173
186
  Stop a generator agent only on evidence of the real failure mode:
174
187
  reasoning runaway, where a turn ends on max_tokens having emitted a
@@ -368,7 +368,7 @@ var UPDATE_PROMPT = {
368
368
  "1. The MCP server updates ITSELF automatically at session start (npx @latest) \u2014 nothing to do for the pipeline.",
369
369
  "2. If a global CLI is installed: `npm install -g @tendrilapp/cli@latest`.",
370
370
  "3. If tendril_doctor reported the SERVER version stale this session: `npm cache clean --force`, then the user restarts the session so npx re-resolves @latest.",
371
- "4. The optional plugin is a thin shim (triggers, agents, commands) that rarely changes \u2014 usually nothing to do. When a plugin update IS announced (third-party marketplaces do not reliably auto-update): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` FIRST (the update command compares against a cached clone and can falsely report already-latest), then `claude plugin update tendril`, then /reload-plugins; VS Code extension \u2014 the /plugins panel has no update button: uninstall tendril, reinstall from the marketplace, reopen the chat panel; Claude Desktop \u2014 no plugin management: quit and relaunch. Every critical surface already updated in step 1 regardless.",
371
+ "4. The optional plugin is a thin shim (triggers, agents, commands) that rarely changes \u2014 usually nothing to do. When a plugin update IS announced (third-party marketplaces do not reliably auto-update): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` FIRST (the update command compares against a cached clone and can falsely report already-latest), then `claude plugin update tendril`, then /reload-plugins; VS Code extension \u2014 the /plugins panel has no update button: Marketplaces tab \u2192 REFRESH tendrilapp FIRST (a stale marketplace clone makes reinstall reinstall the old version \u2014 measured), then Plugins tab \u2192 uninstall tendril, reinstall, reopen the chat panel; if the refresh doesn't take, remove and re-add the marketplace. Claude Desktop \u2014 no plugin management: quit and relaunch. Every critical surface already updated in step 1 regardless.",
372
372
  "5. Confirm by running tendril_doctor and reporting its version line. Never claim success without that confirmation."
373
373
  ].join("\n")
374
374
  };
package/dist/tendril.js CHANGED
@@ -2332,6 +2332,23 @@ function detectBackdrop(png, opts = {}) {
2332
2332
  const { modal, consensus } = ringConsensus(png);
2333
2333
  if (consensus < 0.9) return [255, 255, 255];
2334
2334
  if (Math.min(...modal) < 200 && opts.trustDark !== true) return [255, 255, 255];
2335
+ const decoded = PNG.sync.read(Buffer.from(png));
2336
+ if (decoded.width >= 4 && decoded.height >= 4) {
2337
+ const blocks = cornerBlocks(decoded, 2);
2338
+ const first = blocks[0];
2339
+ const cornersAgree = blocks.every((c) => Math.abs(c[0] - first[0]) + Math.abs(c[1] - first[1]) + Math.abs(c[2] - first[2]) <= INK_DELTA);
2340
+ const cornersDisagreeWithRing = Math.abs(first[0] - modal[0]) + Math.abs(first[1] - modal[1]) + Math.abs(first[2] - modal[2]) > INK_DELTA;
2341
+ if (cornersAgree && cornersDisagreeWithRing) {
2342
+ const extremes = cornerBlocks(decoded, 1);
2343
+ const value = [
2344
+ Math.round(extremes.reduce((a, c) => a + c[0], 0) / 4),
2345
+ Math.round(extremes.reduce((a, c) => a + c[1], 0) / 4),
2346
+ Math.round(extremes.reduce((a, c) => a + c[2], 0) / 4)
2347
+ ];
2348
+ if (Math.min(...value) < 200 && opts.trustDark !== true) return [255, 255, 255];
2349
+ return value;
2350
+ }
2351
+ }
2335
2352
  return modal;
2336
2353
  }
2337
2354
  function pngDimensions(png) {
@@ -7616,8 +7633,8 @@ function authorComponentApi(opts) {
7616
7633
  configs.push({ rep: p.slug, component: componentIdent, props: {} });
7617
7634
  }
7618
7635
  if (inexpressible.length > 0) throw new PoseCompletenessError(inexpressible);
7619
- if (opts.dismissible === true) {
7620
- props.push({ name: "onDismiss", kind: "callback" });
7636
+ if (opts.dismissible !== void 0) {
7637
+ props.push({ name: "onDismiss", kind: "callback", default: opts.dismissible });
7621
7638
  }
7622
7639
  const entry = `${componentIdent}.tsx`;
7623
7640
  const apiPin = {
@@ -7626,13 +7643,15 @@ function authorComponentApi(opts) {
7626
7643
  name: pr.name,
7627
7644
  type: pr.kind === "boolean" ? "boolean" : pr.kind === "string" ? "string" : pr.kind === "callback" ? "() => void" : pr.values.map((v) => `"${v}"`).join(" | "),
7628
7645
  required: false,
7629
- ...pr.default !== void 0 ? { default: pr.default } : {}
7646
+ // A callback's `default` field carries detection EVIDENCE for the
7647
+ // prop line, not a value — it never enters the pin.
7648
+ ...pr.default !== void 0 && pr.kind !== "callback" ? { default: pr.default } : {}
7630
7649
  })),
7631
7650
  forcedStates,
7632
7651
  poseCompleteness: { recordedPoses: opts.poses.length, expressible: configs.length }
7633
7652
  };
7634
7653
  const propLines = props.map(
7635
- (pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 fires on the recorded affordance; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
7654
+ (pr) => pr.kind === "boolean" ? ` ${pr.name}?: boolean;` : pr.kind === "string" ? ` ${pr.name}?: string; // CONTENT \u2014 recorded default ${JSON.stringify(pr.default)}` : pr.kind === "callback" ? ` ${pr.name}?: () => void; // NOTIFICATION \u2014 recorded evidence: ${pr.default ?? "the recorded affordance"}; the component owns its state (it hides/updates itself) and the callback informs, never controls` : ` ${pr.name}?: ${pr.values.map((v) => `"${v}"`).join(" | ")}; // default "${pr.default}"`
7636
7655
  );
7637
7656
  const provided = opts.fonts ?? [];
7638
7657
  const recorded = opts.recordedFonts ?? [];
@@ -7705,7 +7724,7 @@ function authorBehaviors(api, extras = {}) {
7705
7724
  behaviors.push({
7706
7725
  id: `content-prop-renders(${sentinel.prop})`,
7707
7726
  config: sentinel.config,
7708
- props: { [sentinel.prop]: marker },
7727
+ props: { ...sentinel.props ?? {}, [sentinel.prop]: marker },
7709
7728
  steps: [{ assertTextVisible: marker }]
7710
7729
  });
7711
7730
  }
@@ -7823,7 +7842,7 @@ function recordedTextSlots(setDir, repSlugs) {
7823
7842
  if (t === "" || !/[A-Za-z0-9]/.test(t)) continue;
7824
7843
  texts.push(t);
7825
7844
  }
7826
- perRep.push({ slug, texts });
7845
+ perRep.push({ slug, texts, code });
7827
7846
  }
7828
7847
  const axisValuesBySlug = /* @__PURE__ */ new Map();
7829
7848
  for (const slug of repSlugs) {
@@ -7866,7 +7885,9 @@ function recordedTextSlots(setDir, repSlugs) {
7866
7885
  const v = propRep.find((pr) => pr.slug === r.slug)?.props.get(name) ?? def;
7867
7886
  return r.texts.some((t) => t.includes(v)) || r.texts.join(" ").replace(/\s+/g, " ").includes(v.replace(/\s+/g, " ").trim());
7868
7887
  }).map((r) => r.slug);
7869
- return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn };
7888
+ const refPattern = new RegExp(`\\{\\s*${name}\\s*\\}`);
7889
+ const referencedIn = perRep.filter((r) => refPattern.test(r.code)).map((r) => r.slug);
7890
+ return { prop: name, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn, referencedIn };
7870
7891
  });
7871
7892
  }
7872
7893
  if (perRep.length === 0) return [];
@@ -7913,7 +7934,7 @@ function recordedTextSlots(setDir, repSlugs) {
7913
7934
  usedNames.add(prop);
7914
7935
  const overrides = {};
7915
7936
  for (const [slug, v] of sl.values) if (v !== def) overrides[slug] = v;
7916
- return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()] };
7937
+ return { prop, default: def, overrides, varies: Object.keys(overrides).length > 0, visibleIn: [...sl.values.keys()], referencedIn: [] };
7917
7938
  });
7918
7939
  }
7919
7940
  function authorTaskFromSet(setDir, opts = {}) {
@@ -7954,12 +7975,29 @@ function authorTaskFromSet(setDir, opts = {}) {
7954
7975
  ...recordedFonts.length > 0 ? { recordedFonts } : {},
7955
7976
  ...Object.keys(defaults).length > 0 ? { defaults } : {},
7956
7977
  ...textSlots.length > 0 ? { textSlots } : {},
7957
- ...dismissName !== void 0 ? { dismissible: true } : {}
7978
+ ...dismissName !== void 0 ? { dismissible: dismissName } : {}
7958
7979
  });
7959
7980
  const anchorSlug = api.configs.find((c) => Object.keys(c.props).length === 0)?.rep ?? api.configs[0]?.rep;
7960
- const sentinels = textSlots.filter((slot) => !slot.varies && slot.visibleIn.length > 0).map((slot) => {
7981
+ const sentinels = textSlots.filter((slot) => !slot.varies).map((slot) => {
7961
7982
  const authored = api.props.find((pr) => pr.kind === "string" && pr.default === slot.default);
7962
- return authored === void 0 ? void 0 : { prop: authored.name, config: anchorSlug !== void 0 && slot.visibleIn.includes(anchorSlug) ? anchorSlug : slot.visibleIn[0] };
7983
+ if (authored === void 0) return void 0;
7984
+ if (slot.visibleIn.length > 0) {
7985
+ const entry = {
7986
+ prop: authored.name,
7987
+ config: anchorSlug !== void 0 && slot.visibleIn.includes(anchorSlug) ? anchorSlug : slot.visibleIn[0]
7988
+ };
7989
+ return entry;
7990
+ }
7991
+ if (slot.referencedIn.length > 0) {
7992
+ const toggle = api.props.find(
7993
+ (pr) => pr.kind === "boolean" && authored.name.toLowerCase().startsWith(pr.name.toLowerCase()) && authored.name.length > pr.name.length
7994
+ );
7995
+ if (toggle !== void 0 && anchorSlug !== void 0) {
7996
+ const entry = { prop: authored.name, config: anchorSlug, props: { [toggle.name]: true } };
7997
+ return entry;
7998
+ }
7999
+ }
8000
+ return void 0;
7963
8001
  }).filter((x) => x !== void 0);
7964
8002
  const { behaviors, prelude, disclosures } = authorBehaviors(api, { ...sentinels.length > 0 ? { sentinels } : {} });
7965
8003
  if (dismissName !== void 0) {
@@ -7970,10 +8008,15 @@ function authorTaskFromSet(setDir, opts = {}) {
7970
8008
  }
7971
8009
  for (const slot of textSlots) {
7972
8010
  if (slot.varies) continue;
7973
- if (slot.visibleIn.length > 0) {
8011
+ const sentineled = sentinels.some((sn) => api.props.some((pr) => pr.name === sn.prop && pr.default === slot.default));
8012
+ if (sentineled) {
7974
8013
  disclosures.push(
7975
8014
  `constant content prop (recorded ${JSON.stringify(slot.default)}) is sentinel-checked: a behavior mounts it with a sentinel string and asserts it renders \u2014 hardcoding the recorded literal fails that check`
7976
8015
  );
8016
+ } else if (slot.referencedIn.length > 0) {
8017
+ disclosures.push(
8018
+ `content prop (recorded ${JSON.stringify(slot.default)}) is rendered by REFERENCE in the emission but has no visibility evidence and no boolean toggle to reveal it \u2014 sentinel skipped (it could fail honest components whose pose hides the node); wire the prop anyway`
8019
+ );
7977
8020
  } else {
7978
8021
  disclosures.push(
7979
8022
  `content prop from recorded text is UNEXERCISED AND UNRENDERED: NO recorded pose visibly renders ${JSON.stringify(slot.default)} (hidden node) \u2014 prescribed for completeness; wire it behind its visibility toggle; no sentinel can honestly run`
@@ -8378,7 +8421,7 @@ function statusOf(s) {
8378
8421
  return tierOf(s, BARS.cert);
8379
8422
  }
8380
8423
  function emitBundleV1(opts) {
8381
- const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, status: statusOf(s) }));
8424
+ const statuses = opts.scores.map((s) => ({ rep: s.rep, similarity: s.similarity, inkRecall: s.inkRecall, ...s.exact !== void 0 ? { exact: s.exact } : {}, status: statusOf(s) }));
8382
8425
  const certified = statuses.filter((s) => s.status === "certified").length;
8383
8426
  const pass = statuses.filter((s) => s.status !== "fail").length;
8384
8427
  const lattice = countLatticeSymbols(opts.task.set);
@@ -9864,7 +9907,7 @@ async function runDoctorChecks(options) {
9864
9907
  name: "plugin-skew",
9865
9908
  ok: false,
9866
9909
  detail: `Claude Code plugin cache holds v${newest} while this CLI is ${cliVersion()} \u2014 the plugin's skill/agents are STALE and will silently shadow current doctrine`,
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."
9910
+ remediation: "Update the plugin \u2014 REFRESH THE MARKETPLACE FIRST (its local clone goes stale and a reinstall faithfully reinstalls the old version): terminal Claude Code \u2014 `claude plugin marketplace update tendrilapp` then `claude plugin update tendril`; VS Code extension \u2014 /plugins panel \u2192 Marketplaces tab \u2192 refresh tendrilapp, THEN Plugins tab \u2192 uninstall + reinstall tendril, reopen the chat panel. If the refresh doesn't take, remove the tendrilapp marketplace entirely and re-add TendrilApp/claude-plugin (a fresh clone cannot be stale)."
9868
9911
  } : { name: "plugin-skew", ok: true, detail: `Claude Code plugin v${newest} matches this CLI` }
9869
9912
  );
9870
9913
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tendrilapp/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Figma design systems → verified React components. CLI ruler + MCP server.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",