@tendrilapp/cli 0.1.19 → 0.1.20
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 +29 -6
- package/dist/tendril-mcp.js +1 -1
- package/dist/tendril.js +31 -13
- package/package.json +1 -1
package/dist/SKILL.md
CHANGED
|
@@ -20,12 +20,35 @@ 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
Permissions, FIRST THING in every pipeline run (the trigger is a FILE
|
|
24
|
+
CHECK, not prompt-watching — you cannot observe permission prompts, so
|
|
25
|
+
never condition on them; a measured session full of prompts had a
|
|
26
|
+
prompt-conditioned offer never fire): check whether ANY Claude
|
|
27
|
+
settings file the host merges — the project's
|
|
28
|
+
`.claude/settings.local.json` or `.claude/settings.json`, or the
|
|
29
|
+
user's `~/.claude/settings.json` — contains tendril MCP entries
|
|
30
|
+
(plugin installs use `mcp__plugin_tendril_tendril__*`; direct
|
|
31
|
+
`claude mcp add` installs use `mcp__<server-name>__*`). If none does,
|
|
32
|
+
BEFORE the first recording call, tell the user one approval can
|
|
33
|
+
replace every pipeline prompt and offer `tendril_permissions` with
|
|
34
|
+
`write: true` — it merges the per-tool allowlist into the project's
|
|
35
|
+
settings.local.json (idempotent, touches nothing else; note it writes
|
|
36
|
+
PLUGIN-prefixed names — a direct-install user should run
|
|
37
|
+
`tendril permissions --claude` and adapt the prefix instead) and the
|
|
38
|
+
user must reload the session to apply; say that out loud. Offer ONCE
|
|
39
|
+
per session; never run it unoffered (it edits the user's settings),
|
|
40
|
+
and if declined, proceed without mentioning it again.
|
|
41
|
+
|
|
42
|
+
Long generator/score waits: silence is not a health signal. The scorer
|
|
43
|
+
appends a timestamped line to `<candidateDir>/score-history.jsonl`
|
|
44
|
+
after EVERY completed round — the file APPEARS ONLY AFTER THE FIRST
|
|
45
|
+
score, so its absence early in a run is normal, not frozen. Poll it
|
|
46
|
+
(and the candidate dir's mtimes) about every 30 seconds and relay
|
|
47
|
+
progress to the user ("round 3 scored: 15/21 at floor 0.885"). A
|
|
48
|
+
healthy run can take 30+ minutes; never kill on elapsed time alone —
|
|
49
|
+
count staleness from the LAST CHANGE to either signal, not from run
|
|
50
|
+
start, and when past ~20 minutes of true staleness, ask the user
|
|
51
|
+
before killing.
|
|
29
52
|
|
|
30
53
|
Batch runs (several components in one session):
|
|
31
54
|
- Cap concurrent recorders at FOUR. All recorders share one Figma
|
package/dist/tendril-mcp.js
CHANGED
|
@@ -66,7 +66,7 @@ var TOOLS = [
|
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
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.
|
|
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. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* (for those, write:true installs PLUGIN-prefixed names that will not match: list without writing and adapt the prefix instead). A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
70
70
|
schema: z.object({
|
|
71
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
72
|
}),
|
package/dist/tendril.js
CHANGED
|
@@ -8256,7 +8256,7 @@ function dismissEvidence(setDir, repSlugs) {
|
|
|
8256
8256
|
}
|
|
8257
8257
|
return void 0;
|
|
8258
8258
|
}
|
|
8259
|
-
function recordedFontNeeds(setDir) {
|
|
8259
|
+
function recordedFontNeeds(setDir, opts = {}) {
|
|
8260
8260
|
const byFamily = /* @__PURE__ */ new Map();
|
|
8261
8261
|
const unpaired = /* @__PURE__ */ new Set();
|
|
8262
8262
|
const famAdd = (raw, weight) => {
|
|
@@ -8298,8 +8298,8 @@ function recordedFontNeeds(setDir) {
|
|
|
8298
8298
|
if (existsSync21(defs)) fromDefs(envelopeText(defs));
|
|
8299
8299
|
}
|
|
8300
8300
|
return [...byFamily.entries()].map(([family, paired]) => {
|
|
8301
|
-
const weights = /* @__PURE__ */ new Set([...paired, ...unpaired]);
|
|
8302
|
-
return { family, weights: weights.size === 0 ? [400] : [...weights].sort((a, b) => a - b) };
|
|
8301
|
+
const weights = /* @__PURE__ */ new Set([...paired, ...opts.pairedOnly === true ? [] : unpaired]);
|
|
8302
|
+
return { family, weights: weights.size === 0 ? opts.pairedOnly === true ? [] : [400] : [...weights].sort((a, b) => a - b) };
|
|
8303
8303
|
});
|
|
8304
8304
|
}
|
|
8305
8305
|
function symbolFontGlyphCount(setDir, reps) {
|
|
@@ -9261,10 +9261,19 @@ function unprovisionedFamilies(setDir, cacheDir) {
|
|
|
9261
9261
|
const provided = new Set(verifiedFontFamilies(cacheDir).map((f) => f.family.toLowerCase()));
|
|
9262
9262
|
return declared.filter((f) => !provided.has(f.toLowerCase()));
|
|
9263
9263
|
}
|
|
9264
|
+
function unprovisionedFaces(setDir, cacheDir) {
|
|
9265
|
+
return [
|
|
9266
|
+
...unprovisionedFamilies(setDir, cacheDir),
|
|
9267
|
+
...missingWeights(setDir, cacheDir).map((g) => {
|
|
9268
|
+
const missing = g.declared.filter((w) => !g.provided.includes(w));
|
|
9269
|
+
return `${g.family} (${missing.length === 1 ? "weight" : "weights"} ${missing.join(", ")})`;
|
|
9270
|
+
})
|
|
9271
|
+
];
|
|
9272
|
+
}
|
|
9264
9273
|
function missingWeights(setDir, cacheDir) {
|
|
9265
9274
|
let needs;
|
|
9266
9275
|
try {
|
|
9267
|
-
needs = recordedFontNeeds(setDir);
|
|
9276
|
+
needs = recordedFontNeeds(setDir, { pairedOnly: true });
|
|
9268
9277
|
} catch {
|
|
9269
9278
|
return [];
|
|
9270
9279
|
}
|
|
@@ -9468,7 +9477,8 @@ async function runVerify(opts) {
|
|
|
9468
9477
|
} else if (opts.bar === "cert" && taskFontFamilies(task.set) === null) {
|
|
9469
9478
|
warn(opts, "family coverage could not be established from this recording (no declared families) \u2014 the certification gate covers only cache non-emptiness here");
|
|
9470
9479
|
}
|
|
9471
|
-
|
|
9480
|
+
const weightGaps = missingWeights(task.set);
|
|
9481
|
+
for (const g of weightGaps) {
|
|
9472
9482
|
warn(opts, `font weights (advisory): the recording implies weights ${g.declared.join(", ")} for "${g.family}" \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (certification stays family-gated; implied weights can leak across families)`);
|
|
9473
9483
|
}
|
|
9474
9484
|
const missing = task.configs.filter(
|
|
@@ -9580,7 +9590,8 @@ async function runVerify(opts) {
|
|
|
9580
9590
|
// Machine-readable cause (adversarial review): a cert-bar failure
|
|
9581
9591
|
// with zero pixel/behavior/composition failures was only
|
|
9582
9592
|
// explainable from stderr prose.
|
|
9583
|
-
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {}
|
|
9593
|
+
...certBlockedByAbsentInk.length > 0 ? { certBlockedByAbsentInk } : {},
|
|
9594
|
+
...weightGaps.length > 0 ? { fontWeightGaps: weightGaps } : {}
|
|
9584
9595
|
};
|
|
9585
9596
|
emitData(opts, report, () => {
|
|
9586
9597
|
for (const s of statuses) {
|
|
@@ -9672,9 +9683,15 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
|
|
|
9672
9683
|
const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path31.join(opts.bundleDir, f)).filter((f) => existsSync25(f)).map((f) => readFileSync22(f, "utf8")).join("\n")));
|
|
9673
9684
|
if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
|
|
9674
9685
|
process.stdout.write(`fonts: scored with Tendril-cache faces \u2014 a consuming app must provision the same families (the bundle ships fonts.css when faces are shippable; sha-pinned list in component.json requiredFonts)
|
|
9686
|
+
`);
|
|
9687
|
+
}
|
|
9688
|
+
for (const g of weightGaps) {
|
|
9689
|
+
process.stdout.write(`fonts (weight gap): "${g.family}" declares ${g.declared.join(", ")} \u2014 cache provides ${g.provided.join(", ")}; nearest-weight rendering may shift stroke density (advisory, never a gate). Fix: tendril fonts resolve --set ${task.set}
|
|
9675
9690
|
`);
|
|
9676
9691
|
}
|
|
9677
9692
|
process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
|
|
9693
|
+
`);
|
|
9694
|
+
process.stdout.write(`eye check: tendril inspect ${opts.bundleDir} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape
|
|
9678
9695
|
`);
|
|
9679
9696
|
});
|
|
9680
9697
|
if (opts.bar === "cert" && substitutedFamilies.length > 0) {
|
|
@@ -9780,8 +9797,9 @@ DO NOT INVENT PIXELS FOR THESE POSES. Implement them ONLY as the composition of
|
|
|
9780
9797
|
${notRecorded}` : "";
|
|
9781
9798
|
let fontProvisioning;
|
|
9782
9799
|
if (existsSync26(manifestPath2)) {
|
|
9783
|
-
const
|
|
9784
|
-
const unprovided =
|
|
9800
|
+
const missingFams = unprovisionedFamilies(task.set);
|
|
9801
|
+
const unprovided = unprovisionedFaces(task.set);
|
|
9802
|
+
const weightOnly = missingFams.length === 0;
|
|
9785
9803
|
if (unprovided.length > 0) {
|
|
9786
9804
|
const names = unprovided.map((f) => `'${f}'`).join(", ");
|
|
9787
9805
|
const PROPRIETARY = /^(sf pro|sf compact|sf mono|new york|pingfang|segoe ui|helvetica neue|proxima nova|avenir)/i;
|
|
@@ -9789,13 +9807,13 @@ ${notRecorded}` : "";
|
|
|
9789
9807
|
fontProvisioning = {
|
|
9790
9808
|
unprovided,
|
|
9791
9809
|
question: {
|
|
9792
|
-
prompt: `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9810
|
+
prompt: weightOnly ? `This design implies font ${unprovided.length === 1 ? "weight" : "weights"} the local kit lacks: ${names} (the ${missingFams.length === 0 && unprovided.length === 1 ? "family itself is" : "families themselves are"} provisioned). How should it be handled?` : `This design's text uses ${names}, which is not in the local font kit. How should it be handled?`,
|
|
9793
9811
|
options: [
|
|
9794
|
-
allProprietary ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9795
|
-
'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
9812
|
+
allProprietary && !weightOnly ? `(Recommended) ${names} ${unprovided.length === 1 ? "is a proprietary face" : "are proprietary faces"} \u2014 Google Fonts cannot serve ${unprovided.length === 1 ? "it" : "them"}, so skip \`fonts resolve\`: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold a licence for (the file never leaves this machine), then re-run this brief \u2014 exact text fidelity.` : `(Recommended) Run \`tendril fonts resolve --set ${task.set}\` \u2014 open-source families are fetched automatically. Any face that fails there is one you license privately: run \`tendril fonts add "<Family>" <weight> <file>\` with the .woff2/.ttf/.otf you hold (the file never leaves this machine). Then re-run this brief \u2014 exact text fidelity.`,
|
|
9813
|
+
weightOnly ? "Continue with nearest-weight rendering: the family is provisioned, certification remains possible, and the gap rides the report as an advisory (fontWeightGaps); resolving removes it." : 'Continue with a substitute face: generation proceeds in a provided family, no config can score "certified" under it (cert-bar runs exit fonts-unproven after their report), the substitution is disclosed, and small text differences are expected and not fixable from CSS.'
|
|
9796
9814
|
]
|
|
9797
9815
|
},
|
|
9798
|
-
nonInteractive: opts.bar === "cert" ? `Resolve the families first (\`tendril fonts resolve --set ${task.set}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
9816
|
+
nonInteractive: weightOnly ? `Run \`tendril fonts resolve --set ${task.set}\` if quick, or continue \u2014 the weight gap is an advisory, never a gate.` : opts.bar === "cert" ? `Resolve the families first (\`tendril fonts resolve --set ${task.set}\`) \u2014 this loop targets the cert bar, and scoring exits fonts-unproven under a substitute, so continuing without resolving is a dead end.` : "Continue with the substitute and state the substitution in your report."
|
|
9799
9817
|
};
|
|
9800
9818
|
}
|
|
9801
9819
|
}
|
|
@@ -10273,7 +10291,7 @@ var init_server = __esm({
|
|
|
10273
10291
|
},
|
|
10274
10292
|
{
|
|
10275
10293
|
name: "tendril_permissions",
|
|
10276
|
-
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.
|
|
10294
|
+
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. OFFER THIS AT PIPELINE START whenever NO merged Claude settings file (project .claude/settings.local.json or .claude/settings.json, or user ~/.claude/settings.json) contains tendril MCP entries \u2014 plugin installs use mcp__plugin_tendril_tendril__*, direct claude-mcp-add installs use mcp__<server>__* (for those, write:true installs PLUGIN-prefixed names that will not match: list without writing and adapt the prefix instead). A FILE check, never prompt-watching \u2014 agents cannot observe permission prompts. ONE approval here replaces a prompt per pipeline call. Never run it unoffered; the user must reload the session for new settings to apply \u2014 say so.",
|
|
10277
10295
|
schema: z12.object({
|
|
10278
10296
|
write: z12.boolean().optional().describe("true = install into .claude/settings.local.json (the point of this tool); false/absent = just return the entries")
|
|
10279
10297
|
}),
|