@tendrilapp/cli 0.1.17 → 0.1.19

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
@@ -20,11 +20,36 @@ Non-negotiables (the CLI enforces these; do not fight them):
20
20
  - Sub-bar results are honest, not failures to hide: exit 5 ships the
21
21
  bundle with real scores. Report them as they are.
22
22
 
23
+ First-run friction: if tendril/Figma tool calls are hitting permission
24
+ prompts, tell the user ONE approval can replace them all and offer
25
+ `tendril_permissions` with `write: true` — it merges the pipeline's
26
+ per-tool allowlist into the project's `.claude/settings.local.json`
27
+ (idempotent, touches nothing else; the user reloads the session to
28
+ apply). Never run it unoffered: it edits the user's settings.
29
+
30
+ Batch runs (several components in one session):
31
+ - Cap concurrent recorders at FOUR. All recorders share one Figma
32
+ desktop MCP server; a measured 8-wide batch hit its rate limit
33
+ (per-piece resume recovered, but the stall is avoidable).
34
+ - Batch the questions: plan ALL sets first, then put every
35
+ defaults-to-confirm and the one model question to the user together
36
+ — never one dialog per component.
37
+ - Create candidate directories with a bare `mkdir -p <dir>` — no
38
+ `&&`-compounds. Compound variants each need their own permission
39
+ approval; the bare form is one grant for the whole batch.
40
+ - Trust only tool output for completion state: check
41
+ `tendril_record_status` against disk, never a subagent's prose (a
42
+ measured batch had a recorder report success with zero reps
43
+ recorded).
44
+
23
45
  ## Recording a new component (needs Figma MCP)
24
46
 
25
47
  1. Fetch the component set's frame metadata via Figma MCP
26
- (`get_metadata` on the frame); save the VERBATIM response to a file.
27
- 2. `tendril_record_plan` with that file the queue plan. The plan
48
+ (`get_metadata` on the frame).
49
+ 2. `tendril_record_plan` with the response text passed VERBATIM via
50
+ `metadataParts` (one array entry per response block, in order —
51
+ single-block responses may use `metadata` instead; nothing is saved
52
+ to a file, and never hand-join blocks) → the queue plan. The plan
28
53
  records the FULL variant matrix — every pose the set defines
29
54
  (sampling exists only as a human-run CLI flag; the MCP surface
30
55
  cannot sample). CROSS-CHECK `variantsFound` in the plan output
@@ -51,6 +76,9 @@ Non-negotiables (the CLI enforces these; do not fight them):
51
76
  every open-source face the recording declares, no questions needed.
52
77
  Only faces that FAIL there are a licensing decision for the user:
53
78
  offer `tendril fonts add` (Recommended) or a disclosed substitute.
79
+ A substitute never certifies: scores are capped at pass and a
80
+ cert-bar run exits fonts-unproven after its report — resolving the
81
+ real faces is the only path to certification.
54
82
  3. Record each planned rep with FOUR tool calls: make the THREE
55
83
  Figma MCP calls in protocol order — get_metadata, then
56
84
  get_design_context (excludeScreenshot=true), then get_screenshot —
@@ -25,26 +25,52 @@ var optStr = (d) => z.string().optional().describe(d);
25
25
  var TOOLS = [
26
26
  {
27
27
  name: "tendril_record_plan",
28
- description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from verbatim get_metadata envelope file(s) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. Recording cost is stated in figmaCallEstimate, never asked about \u2014 proceed with what the user provided.",
28
+ description: "ENTRY POINT for implementing/building a React component from a Figma design or figma.com URL \u2014 start the Tendril pipeline here (after loading the tendril skill, if installed). Plans a recording session: computes the rep queue (anchor + one-factor + conflict crosses) from the verbatim get_metadata response (pass its blocks via metadataParts \u2014 no file to write) and persists the set manifest. Resumes if the set already exists. The output may carry USER QUESTIONS \u2014 defaultsToConfirm (which pose is the component's default) or a multiple-component-sets error (which set to record): render them to a present user and apply the answers via `defaults` / `componentSet`; non-interactive runs follow each question's stated fallback. Recording cost is stated in figmaCallEstimate, never asked about \u2014 proceed with what the user provided.",
29
29
  schema: z.object({
30
30
  setDir: str("recording set directory to create/resume"),
31
31
  component: str("component/system name"),
32
- metadataFiles: z.array(z.string()).describe('verbatim get_metadata envelope file paths \u2014 the tool response saved AS-IS, JSON shape {"content":[{"type":"text","text":"<frame \u2026>"}]}; raw copied text is rejected. Optionally <file>@<frameId>'),
32
+ // Parts array FIRST, same reason as ingest_rep: real responses
33
+ // are usually multi-block, and the file param made plan the ONE
34
+ // remaining hand-built-envelope entry point (run 10: the agent
35
+ // wrote the file twice — once as text, once as JSON envelope).
36
+ metadataParts: z.array(z.string()).optional().describe("the frame-level get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join or save them yourself; this is the NORMAL param"),
37
+ metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
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'),
33
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)`),
34
40
  componentSet: optStr("record only this component set (the user's pick from a multiple-component-sets error)")
35
41
  }),
36
- argv: (i) => [
37
- "record",
38
- "plan",
39
- "--set",
40
- i["setDir"],
41
- "--component",
42
- i["component"],
43
- "--metadata",
44
- ...i["metadataFiles"],
45
- ...i["defaults"] !== void 0 ? ["--default", ...i["defaults"]] : [],
46
- ...i["componentSet"] !== void 0 ? ["--component-set", i["componentSet"]] : []
47
- ]
42
+ // Texts ride temp files, never argv: Windows caps a command line at
43
+ // ~32 KB and metadata envelopes can exceed it.
44
+ argv: (i) => {
45
+ const argvOut = ["record", "plan", "--set", i["setDir"], "--component", i["component"]];
46
+ if (i["metadata"] !== void 0 && i["metadataParts"] !== void 0) throw new Error("pass at most one of `metadata` and `metadataParts`");
47
+ const files = i["metadataFiles"];
48
+ if (files !== void 0 && files.length > 0) argvOut.push("--metadata", ...files);
49
+ const single = i["metadata"];
50
+ const parts = i["metadataParts"];
51
+ if (single !== void 0 || parts !== void 0) {
52
+ const tmp = path.join(mkdtempSync(path.join(os.tmpdir(), "tendril-envelope-")), "response.txt");
53
+ if (single !== void 0) {
54
+ writeFileSync(tmp, single);
55
+ argvOut.push("--metadata-raw-file", tmp);
56
+ } else {
57
+ if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
58
+ writeFileSync(tmp, JSON.stringify(parts));
59
+ argvOut.push("--metadata-raw-parts-file", tmp);
60
+ }
61
+ }
62
+ 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
+ return argvOut;
65
+ }
66
+ },
67
+ {
68
+ name: "tendril_permissions",
69
+ description: "Install the Claude Code permission allowlist for the Tendril pipeline (write: true \u2014 merges per-tool entries into the project's .claude/settings.local.json, idempotent, never touches other settings), or list the entries without writing. USE THIS when tendril/Figma tool calls keep hitting permission prompts: offer it to the user once \u2014 ONE approval here replaces a prompt per pipeline call (run 8 measured 16+ prompts for a single component's recording). The user must reload the session for new settings to apply.",
70
+ schema: z.object({
71
+ write: z.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
72
+ }),
73
+ argv: (i) => ["permissions", "--claude", ...i["write"] === true ? ["--write"] : []]
48
74
  },
49
75
  {
50
76
  name: "tendril_doctor",
@@ -81,7 +107,7 @@ var TOOLS = [
81
107
  // multi-block (run 6: 49/49 reps — metadata 2 blocks, design
82
108
  // context 5-6), so the arrays are the norm and the single-string
83
109
  // params the rare case, not the reverse.
84
- metadataParts: z.array(z.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Real responses are almost always multi-block; this is the NORMAL param."),
110
+ metadataParts: z.array(z.string()).optional().describe("get_metadata response blocks, every block in order, each verbatim \u2014 never hand-join blocks yourself. Multi-block responses are common (run 6: 49/49 reps); a single block wrapped in a one-element array is equally fine."),
85
111
  contextParts: z.array(z.string()).optional().describe("get_design_context response blocks, every block in order, each verbatim \u2014 the NORMAL param (real responses arrive as 5-6 blocks)"),
86
112
  screenshotUrl: optStr("image_url from the get_screenshot response, verbatim \u2014 the CLI downloads it; the bytes never pass through your context"),
87
113
  metadata: optStr("ONLY when get_metadata genuinely returned one single block: its text verbatim (otherwise use metadataParts)"),
@@ -241,7 +267,7 @@ var TOOLS = [
241
267
  },
242
268
  {
243
269
  name: "tendril_verify",
244
- description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means sub-bar with an honest report.",
270
+ description: "Verify/check that a component matches its Figma design \u2014 use when the user asks whether an implementation is faithful to the design, or to re-certify an existing Tendril bundle. Recomputes full verification (per-config status, behaviors, composition, evidence artifacts). Free, account-less, network-less \u2014 the trust anchor. Exit 5 means below the target bar with an honest report \u2014 sub-bar scores, or at bar cert a config demoted by absent-ink clusters.",
245
271
  schema: z.object({
246
272
  bundleDir: str("bundle directory to verify"),
247
273
  bar: optStr("pass (default) or cert"),
@@ -343,7 +369,7 @@ function toolResult(result) {
343
369
  const body = (result.stdout.trim() !== "" ? result.stdout : result.stderr) + stale;
344
370
  if (result.exitCode === 5) {
345
371
  return { content: [{ type: "text", text: `${body}
346
- {"exitCode":5,"note":"sub-bar HONEST result \u2014 the report and bundle are valid; this is not a tool failure"}` }] };
372
+ {"exitCode":5,"note":"HONEST below-target result \u2014 the report and bundle are valid; this is not a tool failure. Either sub-bar scores, or (at --bar cert) a config demoted by absent-ink clusters despite at-bar numbers \u2014 the report names which."}` }] };
347
373
  }
348
374
  const text = result.ok ? body : `${body}
349
375
  {"exitCode":${result.exitCode},"note":"CLI exit-code contract: 3 input, 4 confirmation required, 6 fonts unproven, 7 recording incomplete"}`;
@@ -379,7 +405,7 @@ var IMPLEMENT_PROMPT = {
379
405
  build: (figmaUrl) => [
380
406
  `Implement the Figma component at ${figmaUrl} as a pixel-verified React component using the Tendril pipeline.`,
381
407
  "- If the tendril skill (or plugin) is installed, load it and follow it.",
382
- "- Otherwise: start at tendril_record_plan; while recording, make exactly the Figma call each tendril_record_next step names (get_metadata comes before any get_design_context) and save responses verbatim; then tendril_engine_brief, implement the candidate, and tendril_engine_score until the bar passes; finish with tendril_verify.",
408
+ "- Otherwise: start at tendril_record_plan; while recording, make exactly the Figma call each tendril_record_next step names (get_metadata comes before any get_design_context) and pass every response VERBATIM to the ingest tools (no files to save \u2014 the parts params take the response blocks directly); then tendril_engine_brief, implement the candidate, and tendril_engine_score until the bar passes; finish with tendril_verify.",
383
409
  "- Verdicts come only from Tendril's scores \u2014 never claim or estimate fidelity numbers yourself."
384
410
  ].join("\n")
385
411
  };