@tendrilapp/cli 0.1.48 → 0.1.49

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
@@ -1841,6 +1841,29 @@ function emissionTails(setDir, repSlug) {
1841
1841
  }
1842
1842
  return byHead;
1843
1843
  }
1844
+ function restInstancePoses(setDir, repSlug, nodeId) {
1845
+ const repDir = path4.join(setDir, repSlug);
1846
+ if (!resolveRepEnvelopePathIn(repDir, "metadata").endsWith(REST_METADATA_FILE)) return /* @__PURE__ */ new Map();
1847
+ const file = path4.join(repDir, REST_NODES_FILE);
1848
+ if (!existsSync4(file)) return /* @__PURE__ */ new Map();
1849
+ let doc;
1850
+ try {
1851
+ const parsed = JSON.parse(readFileSync3(file, "utf8"));
1852
+ doc = parsed.nodes?.[nodeId]?.document;
1853
+ } catch {
1854
+ return /* @__PURE__ */ new Map();
1855
+ }
1856
+ const out = /* @__PURE__ */ new Map();
1857
+ const walk2 = (n) => {
1858
+ if (n.type === "INSTANCE") {
1859
+ if (typeof n.id === "string" && typeof n.componentId === "string") out.set(n.id, n.componentId);
1860
+ return;
1861
+ }
1862
+ if (Array.isArray(n.children)) for (const c of n.children) walk2(c);
1863
+ };
1864
+ if (doc !== null && typeof doc === "object") walk2(doc);
1865
+ return out;
1866
+ }
1844
1867
  function hostInstances(setDir, repSlug) {
1845
1868
  const metaFile = resolveRepEnvelopePathIn(path4.join(setDir, repSlug), "metadata");
1846
1869
  if (!existsSync4(metaFile)) return [];
@@ -1871,10 +1894,11 @@ function composeReport(index) {
1871
1894
  }
1872
1895
  }
1873
1896
  for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
1874
- void variantNodeId;
1875
1897
  const instances = hostInstances(host.dir, slug);
1876
1898
  if (instances.length === 0) continue;
1877
1899
  const tailsByHead = emissionTails(host.dir, slug);
1900
+ const cidByInstance = restInstancePoses(host.dir, slug, variantNodeId);
1901
+ const repIsMcpRecorded = !resolveRepEnvelopePathIn(path4.join(host.dir, slug), "metadata").endsWith(REST_METADATA_FILE);
1878
1902
  for (const inst of instances) {
1879
1903
  if (visibleEver.get(inst.id) !== true) {
1880
1904
  edges.push({
@@ -1891,10 +1915,12 @@ function composeReport(index) {
1891
1915
  continue;
1892
1916
  }
1893
1917
  const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
1918
+ const cid = cidByInstance.get(inst.id);
1894
1919
  const disclosures = [];
1895
1920
  const refused = [];
1896
1921
  const idCands = index.filter((c) => {
1897
1922
  if (c.dir === host.dir || sameComponent(c, host)) return false;
1923
+ if (cid !== void 0 && c.variantNodeIds.has(cid)) return true;
1898
1924
  for (const t of tails.keys()) if (c.ownIds.has(t)) return true;
1899
1925
  return false;
1900
1926
  });
@@ -1942,7 +1968,15 @@ function composeReport(index) {
1942
1968
  partners: nameProps.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
1943
1969
  disclosures: [
1944
1970
  ...disclosures,
1945
- `NAME-ONLY proposal \u2014 identity unproven, name evidence never auto-joins: instance "${inst.name}" names ${nameProps.map(kitLabel).join(" / ")}. Name-only edges are NOT confirmable (there is nothing sound to confirm); record the partner so an id-backed join can form, or ignore`
1971
+ `NAME-ONLY proposal \u2014 identity unproven, name evidence never auto-joins: instance "${inst.name}" names ${nameProps.map(kitLabel).join(" / ")}. Name-only edges are NOT confirmable (there is nothing sound to confirm); record the partner so an id-backed join can form, or ignore`,
1972
+ // The remediation is CONDITIONAL (review): componentId
1973
+ // is a host-file-namespace id, so the promise holds
1974
+ // only for same-file partners — and only when the host
1975
+ // rep was recorded over MCP (a REST-recorded rep with
1976
+ // no binding genuinely has none to capture).
1977
+ ...repIsMcpRecorded && host.figmaFile !== void 0 && nameProps.some((p) => p.figmaFile === host.figmaFile) ? [
1978
+ `this host records over MCP, so Figma's instance\u2192component binding was never captured \u2014 re-recording the HOST over the REST channel makes this pair id-backed (a fresh recording, then a full host regeneration and re-verify)`
1979
+ ] : []
1946
1980
  ]
1947
1981
  });
1948
1982
  } else if (disclosures.length > 0) {
@@ -1965,8 +1999,10 @@ function composeReport(index) {
1965
1999
  const group = eligible[0];
1966
2000
  const ownedDepths = [];
1967
2001
  for (const [t, d] of tails) if (group.some((m) => m.ownIds.has(t))) ownedDepths.push(d);
1968
- const substitution = ownedDepths.length > 0 && ownedDepths.every((d) => d === 1);
2002
+ const cidBacked = cid !== void 0 && group.some((m) => m.variantNodeIds.has(cid));
2003
+ const substitution = (ownedDepths.length > 0 || cidBacked) && ownedDepths.every((d) => d === 1);
1969
2004
  const poseVariants = /* @__PURE__ */ new Set();
2005
+ if (cidBacked) poseVariants.add(cid);
1970
2006
  for (const m of group) {
1971
2007
  for (const [t, d] of tails) {
1972
2008
  if (d !== 1 || !m.ownIds.has(t)) continue;
@@ -1977,6 +2013,9 @@ function composeReport(index) {
1977
2013
  }
1978
2014
  }
1979
2015
  }
2016
+ if (cid !== void 0 && !cidBacked && ownedDepths.length > 0) {
2017
+ disclosures.push(`Figma's instance binding names an UNRECORDED variant (${cid}) \u2014 this join stands on emission-id evidence alone`);
2018
+ }
1980
2019
  for (const p of nameProps) {
1981
2020
  if (!group.some((m) => sameComponent(m, p))) {
1982
2021
  disclosures.push(`NAME DISAGREES: instance name "${inst.name}" matches ${kitLabel(p)} while id evidence joins ${kitLabel(group[0])} \u2014 if the id join looks wrong, this is the signal`);
@@ -2020,7 +2059,7 @@ function composeReport(index) {
2020
2059
  },
2021
2060
  disclosures: [
2022
2061
  ...disclosures,
2023
- "substitution-grade: every owned tail at depth 1; pixel-neutrality is NOT asserted here \u2014 instance overrides are measured real, and a per-region check is still future work (whole-frame pixels remain the evidence for composed regions)"
2062
+ `substitution-grade (${[...ownedDepths.length > 0 ? ["every owned emission id at depth 1"] : [], ...cidBacked ? ["Figma's instance\u2192component binding (componentId), recorded verbatim over REST"] : []].join(" + ")}); pixel-neutrality is NOT asserted here \u2014 instance overrides are measured real, and a per-region check is still future work (whole-frame pixels remain the evidence for composed regions)`
2024
2063
  ]
2025
2064
  });
2026
2065
  }
@@ -2108,7 +2147,7 @@ function confirmedCompositionStatus(hostSet) {
2108
2147
  status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
2109
2148
  instances: entry.instances,
2110
2149
  affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
2111
- detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}. If the instance is HIDDEN in every recorded pose, this confirmation predates the visibility principle (Cycle A) and re-running compose cannot re-open it (asked-once): either re-record a pose that SHOWS the instance, or remove this entry from the manifest's compositions array by hand \u2014 a retire flag is a named follow-up`
2150
+ detail: ok ? stale ? "the partner manifest changed since the decision (pinned bytes differ) \u2014 the CURRENT recordings still support every confirmed edge" : "the recordings support every confirmed edge (identity, rep attribution and pose re-derived)" : `${unsupported.length} confirmed instance(s) are NOT supported by the current recordings (${unsupported.map((i) => `${i.hostRep}/${i.instanceId}`).join(", ")}) \u2014 a confirmed composition the evidence does not derive; ${remediation}. If the instance is HIDDEN in every recorded pose, this confirmation predates the visibility principle (Cycle A) and re-running compose cannot re-open it (asked-once): either re-record a pose that SHOWS the instance, or remove this entry from the manifest's compositions array by hand \u2014 a retire flag is a named follow-up${unsupported.some((i) => !existsSync4(path4.join(hostSet, i.hostRep, REST_NODES_FILE))) ? ". If this pair was confirmed from REST-recorded evidence and the host was since re-recorded over MCP, the evidence CLASS is gone rather than broken \u2014 re-record the host over the REST channel to restore it" : ""}`
2112
2151
  });
2113
2152
  }
2114
2153
  return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
@@ -2116,6 +2155,105 @@ function confirmedCompositionStatus(hostSet) {
2116
2155
  function createHashHex(bytes) {
2117
2156
  return createHash2("sha256").update(bytes).digest("hex");
2118
2157
  }
2158
+ function liteIdentity(dir) {
2159
+ try {
2160
+ const manifest = loadManifest(dir);
2161
+ return {
2162
+ dir,
2163
+ displayName: manifest.component,
2164
+ ...manifest.figmaFile !== void 0 ? { figmaFile: manifest.figmaFile } : {},
2165
+ ...manifest.componentSetNode !== void 0 ? { componentSetNode: manifest.componentSetNode } : {},
2166
+ variantNodeIds: new Set(manifest.reps.map((r) => r.nodeId)),
2167
+ repSlugByVariantNode: new Map(manifest.reps.map((r) => [r.nodeId, r.slug])),
2168
+ ownIdsByRep: /* @__PURE__ */ new Map(),
2169
+ ownIds: /* @__PURE__ */ new Set()
2170
+ };
2171
+ } catch {
2172
+ return void 0;
2173
+ }
2174
+ }
2175
+ function partnerOpportunities(partnerSet, roots) {
2176
+ const abs = path4.resolve(partnerSet);
2177
+ const index = buildComposeIndex([.../* @__PURE__ */ new Set([...roots.map((r) => path4.resolve(r)), abs])]);
2178
+ const partnerEntry = index.find((e) => path4.resolve(e.dir) === abs);
2179
+ if (partnerEntry === void 0) return { opportunities: [], hostsWithUnreadableDecisions: [] };
2180
+ const entryFor = (dir) => index.find((e) => path4.resolve(e.dir) === path4.resolve(dir)) ?? liteIdentity(path4.resolve(dir));
2181
+ const isPartner = (dirs) => dirs.some((p) => {
2182
+ if (path4.resolve(p.dir) === abs) return true;
2183
+ const e = entryFor(p.dir);
2184
+ return e !== void 0 && sameComponent(e, partnerEntry);
2185
+ });
2186
+ const edges = composeReport(index);
2187
+ const byHost = /* @__PURE__ */ new Map();
2188
+ for (const e of edges) {
2189
+ if (path4.resolve(e.hostSet) === abs) continue;
2190
+ if (e.kind !== "substitution" && e.kind !== "ask" && e.kind !== "proposal") continue;
2191
+ if (!isPartner(e.partners)) continue;
2192
+ const list = byHost.get(e.hostSet) ?? [];
2193
+ list.push(e);
2194
+ byHost.set(e.hostSet, list);
2195
+ }
2196
+ const opportunities = [];
2197
+ const unreadable = [];
2198
+ for (const [hostSet, hostEdges] of byHost) {
2199
+ let decided = false;
2200
+ let malformed = 0;
2201
+ try {
2202
+ const raw = JSON.parse(readFileSync3(path4.join(hostSet, "recording-set.json"), "utf8"));
2203
+ for (const entry of Array.isArray(raw["compositions"]) ? raw["compositions"] : []) {
2204
+ const parsed = CompositionEntrySchema.safeParse(entry);
2205
+ if (!parsed.success) {
2206
+ malformed += 1;
2207
+ continue;
2208
+ }
2209
+ for (const rel of Object.keys(parsed.data.partner.manifestSha256).map(fromStoredRel)) {
2210
+ const e = entryFor(path4.resolve(hostSet, rel));
2211
+ if (e !== void 0 && sameComponent(e, partnerEntry)) decided = true;
2212
+ }
2213
+ }
2214
+ } catch {
2215
+ malformed += 1;
2216
+ }
2217
+ if (malformed > 0) unreadable.push({ hostSet, unreadable: malformed });
2218
+ if (decided) continue;
2219
+ const hostName = index.find((e) => path4.resolve(e.dir) === path4.resolve(hostSet))?.displayName ?? path4.basename(hostSet);
2220
+ const byKind = { substitution: [], ask: [], proposal: [] };
2221
+ for (const e of hostEdges) byKind[e.kind].push(e);
2222
+ if (byKind.substitution.length > 0) {
2223
+ const dirs = byKind.substitution[0].partners.map((p) => p.dir);
2224
+ opportunities.push({
2225
+ hostSet,
2226
+ hostDisplayName: hostName,
2227
+ kind: "confirmable",
2228
+ pairKey: pairKeyFor(path4.resolve(hostSet), dirs),
2229
+ instances: byKind.substitution.length,
2230
+ hostReps: [...new Set(byKind.substitution.map((e) => e.hostRep))],
2231
+ disclosures: [...new Set(byKind.substitution.flatMap((e) => e.disclosures))]
2232
+ });
2233
+ }
2234
+ if (byKind.ask.length > 0) {
2235
+ opportunities.push({
2236
+ hostSet,
2237
+ hostDisplayName: hostName,
2238
+ kind: "ambiguous",
2239
+ instances: byKind.ask.length,
2240
+ hostReps: [...new Set(byKind.ask.map((e) => e.hostRep))],
2241
+ disclosures: [...new Set(byKind.ask.flatMap((e) => e.disclosures))]
2242
+ });
2243
+ }
2244
+ if (byKind.proposal.length > 0) {
2245
+ opportunities.push({
2246
+ hostSet,
2247
+ hostDisplayName: hostName,
2248
+ kind: "name-only",
2249
+ instances: byKind.proposal.length,
2250
+ hostReps: [...new Set(byKind.proposal.map((e) => e.hostRep))],
2251
+ disclosures: [...new Set(byKind.proposal.flatMap((e) => e.disclosures))]
2252
+ });
2253
+ }
2254
+ }
2255
+ return { opportunities, hostsWithUnreadableDecisions: unreadable };
2256
+ }
2119
2257
  var CompositionEntrySchema, norm, FOOTER, kitLabel, toPosixRel, fromStoredRel, pairKeyFor;
2120
2258
  var init_compose = __esm({
2121
2259
  "packages/figma/src/recording/compose.ts"() {
@@ -6652,7 +6790,7 @@ var init_bundle_files = __esm({
6652
6790
  // packages/metadata/src/verify-report.ts
6653
6791
  function verifyReportForTransport(report, bundleName) {
6654
6792
  const { dir: _dir, ...evidence } = report.evidence;
6655
- const { eyeCheck: _eyeCheck, ...rest } = report;
6793
+ const { eyeCheck: _eyeCheck, next: _next, composeOpportunities: _composeOpportunities, ...rest } = report;
6656
6794
  return { ...rest, bundle: bundleName, evidence };
6657
6795
  }
6658
6796
  function nonEmptyArray(value) {
@@ -14439,7 +14577,16 @@ function compositionPairsFor(hostSet, roots) {
14439
14577
  else invalid++;
14440
14578
  }
14441
14579
  const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
14442
- return { open: pairs.filter((p) => !decidedKeys.has(p.key)), standing, invalid, ...skippedParent !== void 0 ? { skippedParent } : {} };
14580
+ const proposalRows = /* @__PURE__ */ new Map();
14581
+ for (const e of edges) {
14582
+ if (e.hostSet !== hostSet || e.kind !== "proposal") continue;
14583
+ const key = e.partners.map((p) => p.dir).sort().join("+");
14584
+ const row = proposalRows.get(key) ?? { displayName: e.partners[0]?.displayName ?? e.instanceName, partnerDirs: e.partners.map((p) => p.dir), instances: 0, disclosures: [] };
14585
+ row.instances += 1;
14586
+ for (const d of e.disclosures) if (!row.disclosures.includes(d)) row.disclosures.push(d);
14587
+ proposalRows.set(key, row);
14588
+ }
14589
+ return { open: pairs.filter((p) => !decidedKeys.has(p.key)), proposals: [...proposalRows.values()], standing, invalid, ...skippedParent !== void 0 ? { skippedParent } : {} };
14443
14590
  }
14444
14591
  function substitutionPairs(edges, hostSet) {
14445
14592
  const pairs = /* @__PURE__ */ new Map();
@@ -14884,7 +15031,7 @@ async function runRecordPlan(opts) {
14884
15031
  ...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
14885
15032
  ...setIdentity
14886
15033
  });
14887
- const channelNote = await assignRestChannel(opts.setDir, manifest, resumed);
15034
+ const channelNote = await assignRestChannel(opts.setDir, manifest, resumed, opts.restClient);
14888
15035
  const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
14889
15036
  const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
14890
15037
  const interactionStatesToConfirm = recordsInteractionState(defaultReports) ? void 0 : interactionDisclosure(manifest.component, defaultReports);
@@ -15074,7 +15221,7 @@ async function runRecordPlan(opts) {
15074
15221
  }
15075
15222
  );
15076
15223
  }
15077
- async function assignRestChannel(setDir, manifest, resumed) {
15224
+ async function assignRestChannel(setDir, manifest, resumed, injected) {
15078
15225
  const current = loadManifest(setDir);
15079
15226
  if (current.channel !== void 0) {
15080
15227
  return { active: true, probeReps: current.probeReps ?? [], note: "REST channel already assigned (frozen with the plan)" };
@@ -15090,7 +15237,7 @@ async function assignRestChannel(setDir, manifest, resumed) {
15090
15237
  note: "REST channel unavailable: the plan carries no --figma-file key \u2014 pass it (from the design URL) to record pixels+geometry over Figma REST and escape the MCP daily limit"
15091
15238
  };
15092
15239
  }
15093
- const client = await figmaRestClient();
15240
+ const client = injected !== void 0 ? { ok: true, value: injected } : await figmaRestClient();
15094
15241
  if (!client.ok) {
15095
15242
  return client.kind === "no-credential" ? {
15096
15243
  active: false,
@@ -15151,7 +15298,7 @@ async function runRecordRestFetch(opts) {
15151
15298
  remediation: `Record them first: ${tendrilCommand(`record next --set ${opts.setDir}`)} names the calls.`
15152
15299
  });
15153
15300
  }
15154
- const client = await figmaRestClient();
15301
+ const client = opts.restClient !== void 0 ? { ok: true, value: opts.restClient } : await figmaRestClient();
15155
15302
  if (!client.ok) {
15156
15303
  fail(opts, ExitCode.General, { error: client.refusal, code: `figma-rest-${client.kind}`, remediation: client.kind === "no-credential" ? `Run ${tendrilCommand("figma-connect")} once, then re-run this.` : "Fix what is named above, or fall back: remove `channel`/`probeReps` from recording-set.json to record over MCP." });
15157
15304
  }
@@ -15654,9 +15801,14 @@ function runRecordStatus(opts) {
15654
15801
  const composition = (() => {
15655
15802
  try {
15656
15803
  const setDir = path45.resolve(opts.setDir);
15657
- const { open, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
15804
+ const { open, proposals, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
15658
15805
  return {
15659
15806
  openPairs: open.map((p) => ({ displayName: p.displayName, pairKey: p.key, instances: p.instances.length })),
15807
+ // Name-only proposals (field, 2026-08-26): the host-side
15808
+ // surface an MCP-recorded host embedding a later partner never
15809
+ // had — not confirmable, but never silent either; the
15810
+ // disclosures carry the conditional re-record remediation.
15811
+ nameOnly: proposals.map((p) => ({ displayName: p.displayName, instances: p.instances, disclosures: p.disclosures })),
15660
15812
  confirmed: standing.filter((s) => s.status === "confirmed").length,
15661
15813
  declined: standing.filter((s) => s.status === "declined").length,
15662
15814
  invalid
@@ -15690,6 +15842,12 @@ function runRecordStatus(opts) {
15690
15842
  process.stdout.write(`COMPOSITION ${composition.confirmed} confirmed partner pair(s) on this set (composed pins apply at generation)
15691
15843
  `);
15692
15844
  }
15845
+ if (!("unavailable" in composition) && composition.nameOnly.length > 0) {
15846
+ for (const p of composition.nameOnly) {
15847
+ process.stdout.write(`COMPOSITION NAME-ONLY: instance name matches recorded component ${p.displayName} (${p.instances} instance(s)) \u2014 identity unproven, NOT confirmable. ${p.disclosures.find((d) => d.includes("re-record")) ?? "Record id evidence to make it confirmable, or ignore."}
15848
+ `);
15849
+ }
15850
+ }
15693
15851
  if (!("unavailable" in composition) && composition.invalid > 0) {
15694
15852
  process.stdout.write(
15695
15853
  `COMPOSITION WARNING: ${composition.invalid} standing composition entr${composition.invalid === 1 ? "y is" : "ies are"} MALFORMED in recording-set.json \u2014 a corrupted DECLINE silently re-opens its pair; repair or remove the entry (it holds a human decision and is provenance-hashed)
@@ -16255,6 +16413,177 @@ var init_profile_input = __esm({
16255
16413
  }
16256
16414
  });
16257
16415
 
16416
+ // packages/cli/src/commands/inspect.ts
16417
+ var inspect_exports = {};
16418
+ __export(inspect_exports, {
16419
+ INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
16420
+ buildInspectSheet: () => buildInspectSheet,
16421
+ runInspect: () => runInspect
16422
+ });
16423
+ import { existsSync as existsSync39, readFileSync as readFileSync35, writeFileSync as writeFileSync17 } from "node:fs";
16424
+ import path49 from "node:path";
16425
+ function readVerifyReport(evidenceDir) {
16426
+ const p = path49.join(evidenceDir, VERIFY_REPORT_FILENAME);
16427
+ if (!existsSync39(p)) return void 0;
16428
+ try {
16429
+ return JSON.parse(readFileSync35(p, "utf8"));
16430
+ } catch {
16431
+ return void 0;
16432
+ }
16433
+ }
16434
+ function verdictBlock(report) {
16435
+ if (report === void 0) {
16436
+ return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
16437
+ }
16438
+ const buckets = /* @__PURE__ */ new Map();
16439
+ for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
16440
+ const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
16441
+ const failed = report.behaviors.filter((b) => !b.pass);
16442
+ return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
16443
+ }
16444
+ function scoreLine(report, rep) {
16445
+ const c = report?.configs.find((x) => x.rep === rep);
16446
+ if (c === void 0) return "";
16447
+ const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
16448
+ const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
16449
+ const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
16450
+ return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
16451
+ }
16452
+ async function runInspect(opts) {
16453
+ if (opts.describe) {
16454
+ printDescription(INSPECT_DESCRIPTION);
16455
+ return;
16456
+ }
16457
+ const bundleDir = path49.resolve(opts.bundleDir);
16458
+ const evidenceDir = path49.join(bundleDir, "verify-evidence");
16459
+ const manifestPath2 = path49.join(bundleDir, "component.json");
16460
+ if (!existsSync39(evidenceDir) || !existsSync39(manifestPath2)) {
16461
+ fail(opts, ExitCode.InputValidation, {
16462
+ error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync39(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
16463
+ code: "no-evidence",
16464
+ remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
16465
+ });
16466
+ }
16467
+ const { manifest } = readBundleManifest(readFileSync35(manifestPath2, "utf8"));
16468
+ if (manifest === void 0) {
16469
+ fail(opts, ExitCode.InputValidation, {
16470
+ error: "component.json did not parse as a bundle manifest",
16471
+ code: "no-mount-contract",
16472
+ remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
16473
+ });
16474
+ }
16475
+ const setDir = path49.resolve(opts.set ?? manifest.provenance.recordingSet.path);
16476
+ const report = readVerifyReport(evidenceDir);
16477
+ const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync39(path49.join(evidenceDir, `${rep}-ref.png`)) && existsSync39(path49.join(evidenceDir, `${rep}-render.png`)));
16478
+ if (reps.length === 0) {
16479
+ fail(opts, ExitCode.InputValidation, {
16480
+ error: "verify-evidence holds no ref/render pairs for this bundle's configs",
16481
+ code: "no-evidence",
16482
+ remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
16483
+ });
16484
+ }
16485
+ const { sheet, crops } = buildInspectSheet({ bundleDir, setDir, title: manifest.name, repCandidates: reps, report, ...opts.maxArea !== void 0 ? { maxArea: opts.maxArea } : {} });
16486
+ emitData(opts, { sheet, configs: reps.length, crops }, () => {
16487
+ process.stdout.write(`inspect sheet: ${sheet}
16488
+ ${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
16489
+ `);
16490
+ });
16491
+ }
16492
+ function buildInspectSheet(input) {
16493
+ const evidenceDir = path49.join(path49.resolve(input.bundleDir), "verify-evidence");
16494
+ const setDir = input.setDir;
16495
+ const report = input.report;
16496
+ const reps = input.repCandidates.filter((rep) => existsSync39(path49.join(evidenceDir, `${rep}-ref.png`)) && existsSync39(path49.join(evidenceDir, `${rep}-render.png`)));
16497
+ let crops = 0;
16498
+ const sections = [];
16499
+ for (const rep of reps) {
16500
+ const ref = new Uint8Array(readFileSync35(path49.join(evidenceDir, `${rep}-ref.png`)));
16501
+ const render = new Uint8Array(readFileSync35(path49.join(evidenceDir, `${rep}-render.png`)));
16502
+ const nodes = smallSemanticNodes(setDir, rep, input.maxArea ?? 1024).slice(0, 12);
16503
+ const cells = [];
16504
+ for (const [i, n] of nodes.entries()) {
16505
+ const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
16506
+ try {
16507
+ writeFileSync17(path49.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
16508
+ writeFileSync17(path49.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
16509
+ } catch {
16510
+ continue;
16511
+ }
16512
+ crops += 1;
16513
+ cells.push(
16514
+ `<figure><figcaption>${esc(n.name)} <code>${esc(n.id)}</code> \xB7 ${n.w}\xD7${n.h}</figcaption><div class="pair"><span><em>recorded</em><img src="./${rep}-inspect-${i}-ref.png" alt="recorded ${esc(n.name)}"></span><span><em>rendered</em><img src="./${rep}-inspect-${i}-render.png" alt="rendered ${esc(n.name)}"></span></div></figure>`
16515
+ );
16516
+ }
16517
+ sections.push(
16518
+ `<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<div class="full"><span><em>recorded</em><img src="./${rep}-ref.png"></span><span><em>rendered</em><img src="./${rep}-render.png"></span><span><em>diff</em><img src="./${rep}-diff.png"></span></div>` + (cells.length > 0 ? `<div class="grid">${cells.join("")}</div>` : `<p class="none">no small recorded nodes in this config's sweep</p>`) + `</section>`
16519
+ );
16520
+ }
16521
+ for (const c of report?.configs ?? []) {
16522
+ if (reps.includes(c.rep)) continue;
16523
+ sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
16524
+ }
16525
+ const sheet = path49.join(evidenceDir, "inspect.html");
16526
+ writeFileSync17(
16527
+ sheet,
16528
+ `<!doctype html><meta charset="utf-8"><title>${esc(input.title)} \u2014 tendril inspect</title><style>
16529
+ body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
16530
+ h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
16531
+ .pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
16532
+ .full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
16533
+ figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
16534
+ .grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
16535
+ .verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
16536
+ .verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
16537
+ .verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
16538
+ .verdict small{font-weight:400;color:#666}
16539
+ .tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
16540
+ .tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
16541
+ .t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
16542
+ .caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
16543
+ .scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
16544
+ .missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
16545
+ </style><h1>${esc(input.title)} \u2014 verdict and evidence</h1>
16546
+ ${verdictBlock(report)}
16547
+ <p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
16548
+ Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
16549
+ whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
16550
+ ${sections.join("\n")}
16551
+ <section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
16552
+ `
16553
+ );
16554
+ return { sheet, configs: reps.length, crops };
16555
+ }
16556
+ var INSPECT_DESCRIPTION, esc;
16557
+ var init_inspect = __esm({
16558
+ "packages/cli/src/commands/inspect.ts"() {
16559
+ "use strict";
16560
+ init_src3();
16561
+ init_src4();
16562
+ init_src5();
16563
+ init_describe();
16564
+ init_invocation();
16565
+ init_output();
16566
+ INSPECT_DESCRIPTION = {
16567
+ name: "inspect",
16568
+ summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
16569
+ args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
16570
+ flags: [
16571
+ { flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
16572
+ { flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
16573
+ { flag: "--json", description: "Machine-readable output" }
16574
+ ],
16575
+ output: {
16576
+ sheet: "string \u2014 path to the generated inspect.html",
16577
+ configs: "number \u2014 configs with evidence found",
16578
+ crops: "number \u2014 detail crop pairs written"
16579
+ },
16580
+ exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
16581
+ examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
16582
+ };
16583
+ esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
16584
+ }
16585
+ });
16586
+
16258
16587
  // packages/cli/src/commands/verify.ts
16259
16588
  var verify_exports = {};
16260
16589
  __export(verify_exports, {
@@ -16278,8 +16607,8 @@ __export(verify_exports, {
16278
16607
  runVerify: () => runVerify,
16279
16608
  verdictCaveatsFor: () => verdictCaveatsFor
16280
16609
  });
16281
- import { existsSync as existsSync39, readFileSync as readFileSync35, rmSync as rmSync7, writeFileSync as writeFileSync17 } from "node:fs";
16282
- import path49 from "node:path";
16610
+ import { existsSync as existsSync40, readFileSync as readFileSync36, readdirSync as readdirSync16, rmSync as rmSync7, writeFileSync as writeFileSync18 } from "node:fs";
16611
+ import path50 from "node:path";
16283
16612
  function interactionCoverage(behaviors) {
16284
16613
  const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
16285
16614
  const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
@@ -16517,15 +16846,17 @@ function compositionReport(input) {
16517
16846
  crops: input.crops ?? { unavailable: cropsUnavailable }
16518
16847
  };
16519
16848
  }
16520
- function eyeCheck(bundleDir) {
16521
- return {
16849
+ function eyeCheck(bundleDir, sheet) {
16850
+ const base = {
16522
16851
  command: tendrilCommand(`inspect "${bundleDir}"`),
16523
- sheetPath: path49.join(bundleDir, "verify-evidence", "inspect.html"),
16852
+ sheetPath: path50.join(bundleDir, "verify-evidence", "inspect.html"),
16524
16853
  note: "magnified recorded-vs-rendered crops of every small node; scores cannot see shape. The command WRITES/refreshes the sheet at sheetPath \u2014 re-run it after every verify; an existing sheet may show stale crops."
16525
16854
  };
16855
+ if (sheet === void 0) return base;
16856
+ return { ...base, sheetBuilt: sheet.built, ...sheet.crops !== void 0 ? { sheetCrops: sheet.crops } : {}, ...sheet.error !== void 0 ? { sheetBuildError: sheet.error } : {} };
16526
16857
  }
16527
16858
  function evidenceArtifacts(evidenceDir, reps) {
16528
- const named = (name) => existsSync39(path49.join(evidenceDir, name)) ? name : null;
16859
+ const named = (name) => existsSync40(path50.join(evidenceDir, name)) ? name : null;
16529
16860
  return {
16530
16861
  legend: named("diff-legend.txt"),
16531
16862
  configs: reps.map((rep) => {
@@ -16573,7 +16904,7 @@ function taskFromManifest(opts, manifest, setDir) {
16573
16904
  const adapterSlugs = Object.keys(manifest.propAdapter);
16574
16905
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
16575
16906
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
16576
- const registry = Object.values(TASKS).find((t) => path49.resolve(t.set) === path49.resolve(setDir));
16907
+ const registry = Object.values(TASKS).find((t) => path50.resolve(t.set) === path50.resolve(setDir));
16577
16908
  const authored = (() => {
16578
16909
  if (registry !== void 0) return void 0;
16579
16910
  try {
@@ -16634,19 +16965,19 @@ function verdictCaveatsFor(input) {
16634
16965
  async function runVerify(opts) {
16635
16966
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
16636
16967
  let recordingSetDrift;
16637
- const setOverride = opts.set !== void 0 ? path49.resolve(callerCwd, opts.set) : void 0;
16638
- opts = { ...opts, bundleDir: path49.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
16639
- if (!existsSync39(opts.bundleDir)) {
16968
+ const setOverride = opts.set !== void 0 ? path50.resolve(callerCwd, opts.set) : void 0;
16969
+ opts = { ...opts, bundleDir: path50.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
16970
+ if (!existsSync40(opts.bundleDir)) {
16640
16971
  fail(opts, ExitCode.InputValidation, {
16641
16972
  error: `bundle directory not found: ${opts.bundleDir}`,
16642
16973
  code: "bundle-missing",
16643
16974
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
16644
16975
  });
16645
16976
  }
16646
- const manifestPath2 = path49.join(opts.bundleDir, "component.json");
16977
+ const manifestPath2 = path50.join(opts.bundleDir, "component.json");
16647
16978
  let manifest;
16648
- if (existsSync39(manifestPath2)) {
16649
- const { manifest: parsed, issues } = readBundleManifest(readFileSync35(manifestPath2, "utf8"));
16979
+ if (existsSync40(manifestPath2)) {
16980
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync36(manifestPath2, "utf8"));
16650
16981
  if (issues.length > 0) {
16651
16982
  fail(opts, ExitCode.InputValidation, {
16652
16983
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -16677,21 +17008,21 @@ async function runVerify(opts) {
16677
17008
  task = registry;
16678
17009
  } else if (manifest !== void 0) {
16679
17010
  const resolveSetDir = (p) => {
16680
- if (path49.isAbsolute(p)) return p;
16681
- const fromRepo = path49.resolve(REPO_ROOT, p);
16682
- if (existsSync39(fromRepo)) return fromRepo;
16683
- return path49.resolve(callerCwd, p);
17011
+ if (path50.isAbsolute(p)) return p;
17012
+ const fromRepo = path50.resolve(REPO_ROOT, p);
17013
+ if (existsSync40(fromRepo)) return fromRepo;
17014
+ return path50.resolve(callerCwd, p);
16684
17015
  };
16685
17016
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
16686
- if (!existsSync39(path49.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path49.resolve(t.set) === path49.resolve(setDir))) {
17017
+ if (!existsSync40(path50.join(setDir, "recording-set.json")) && !Object.values(TASKS).some((t) => path50.resolve(t.set) === path50.resolve(setDir))) {
16687
17018
  fail(opts, ExitCode.RecordingIncomplete, {
16688
17019
  error: `recording set not found or unmanifested: ${setDir}`,
16689
17020
  code: "recording-set-missing",
16690
17021
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
16691
17022
  });
16692
17023
  }
16693
- const registry = Object.values(TASKS).find((t) => path49.resolve(t.set) === path49.resolve(setDir));
16694
- if (registry !== void 0 && !existsSync39(path49.join(setDir, "recording-set.json"))) {
17024
+ const registry = Object.values(TASKS).find((t) => path50.resolve(t.set) === path50.resolve(setDir));
17025
+ if (registry !== void 0 && !existsSync40(path50.join(setDir, "recording-set.json"))) {
16695
17026
  const recordedSlugs = registry.configs.map((c) => c.rep);
16696
17027
  const adapterSlugs = Object.keys(manifest.propAdapter);
16697
17028
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -16723,9 +17054,9 @@ async function runVerify(opts) {
16723
17054
  recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
16724
17055
  }
16725
17056
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
16726
- const p = path49.join(opts.bundleDir, name);
16727
- if (!existsSync39(p)) continue;
16728
- const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync35(p)));
17057
+ const p = path50.join(opts.bundleDir, name);
17058
+ if (!existsSync40(p)) continue;
17059
+ const issues = checkBundleSourceFile(name, new Uint8Array(readFileSync36(p)));
16729
17060
  if (issues.length > 0) {
16730
17061
  fail(opts, ExitCode.InputValidation, {
16731
17062
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -16773,7 +17104,7 @@ async function runVerify(opts) {
16773
17104
  });
16774
17105
  }
16775
17106
  const bar = BARS2[opts.bar];
16776
- const evidenceDir = path49.join(opts.bundleDir, "verify-evidence");
17107
+ const evidenceDir = path50.join(opts.bundleDir, "verify-evidence");
16777
17108
  rmSync7(evidenceDir, { recursive: true, force: true });
16778
17109
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
16779
17110
  const quality = await checkBundleQuality(
@@ -16791,7 +17122,7 @@ async function runVerify(opts) {
16791
17122
  // ASKED, never "follows every convention".
16792
17123
  loadCodebaseProfile(opts, opts.profile) ?? void 0
16793
17124
  );
16794
- const bundleCss = ["tokens.css", "styles.css"].map((f) => path49.join(opts.bundleDir, f)).filter((f) => existsSync39(f)).map((f) => readFileSync35(f, "utf8")).join("\n");
17125
+ const bundleCss = ["tokens.css", "styles.css"].map((f) => path50.join(opts.bundleDir, f)).filter((f) => existsSync40(f)).map((f) => readFileSync36(f, "utf8")).join("\n");
16795
17126
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
16796
17127
  const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs, {
16797
17128
  ...opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {},
@@ -16812,10 +17143,10 @@ async function runVerify(opts) {
16812
17143
  warn(opts, `compositions extension REJECTED (${crossComposition.malformed}) \u2014 the cross-bundle backstop did NOT run over it; repair the manifest entry and re-verify. This is an instrument failure, not a clean bill.`);
16813
17144
  }
16814
17145
  const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
16815
- const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path49.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
17146
+ const verifyPins = crossComposition.rows.length > 0 ? composedPins(task.set, [opts.library !== void 0 ? path50.resolve(verifyCallerCwd, opts.library) : verifyCallerCwd]) : { pins: [], issues: [] };
16816
17147
  const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
16817
17148
  const renderExpectations = verifyPins.pins.map((pin) => ({
16818
- modulePath: path49.join(composedModuleDir(pin.partnerName), pin.entryComponent),
17149
+ modulePath: path50.join(composedModuleDir(pin.partnerName), pin.entryComponent),
16819
17150
  component: pin.entryComponent,
16820
17151
  stampName: `${pin.pairKey}:${pin.entryComponent}`,
16821
17152
  hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
@@ -16874,6 +17205,59 @@ async function runVerify(opts) {
16874
17205
  })();
16875
17206
  const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
16876
17207
  const scoredFiles = digestScoredFiles(opts.bundleDir, manifest !== void 0 ? manifest.entry : task.entry);
17208
+ const verdictWord = ok ? "verified" : "verification-failed";
17209
+ const verdictCaveats = verdictCaveatsFor({
17210
+ latticeBlock,
17211
+ operability: coverage.operability,
17212
+ compositionUnavailable: "unavailable" in compositionBlock,
17213
+ fontsSubstituted: substitutedFamilies.length > 0,
17214
+ pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
17215
+ hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
17216
+ });
17217
+ const composeScan = (() => {
17218
+ if (!ok || opts.bar === "cert" && substitutedFamilies.length > 0) return void 0;
17219
+ try {
17220
+ const setAbs = path50.resolve(task.set);
17221
+ let scannedRoots;
17222
+ let skipped;
17223
+ if (opts.library !== void 0) {
17224
+ scannedRoots = [path50.resolve(opts.library)];
17225
+ } else {
17226
+ const parent = path50.dirname(setAbs);
17227
+ let entries = 0;
17228
+ try {
17229
+ entries = readdirSync16(parent).length;
17230
+ } catch {
17231
+ }
17232
+ if (entries > IMPLICIT_PARENT_SCAN_MAX_ENTRIES) {
17233
+ skipped = { dir: parent, entries };
17234
+ scannedRoots = [];
17235
+ } else {
17236
+ scannedRoots = [parent];
17237
+ }
17238
+ }
17239
+ const scan = scannedRoots.length > 0 ? partnerOpportunities(setAbs, scannedRoots) : { opportunities: [], hostsWithUnreadableDecisions: [] };
17240
+ return { scannedRoots, ...skipped !== void 0 ? { skipped } : {}, ...scan };
17241
+ } catch (err) {
17242
+ return { scannedRoots: [], opportunities: [], hostsWithUnreadableDecisions: [], error: err instanceof Error ? err.message.split("\n")[0] ?? String(err) : String(err) };
17243
+ }
17244
+ })();
17245
+ const green = ok && !(opts.bar === "cert" && substitutedFamilies.length > 0);
17246
+ const sheetOutcome = (() => {
17247
+ if (!green) return { built: false };
17248
+ try {
17249
+ const built = buildInspectSheet({
17250
+ bundleDir: opts.bundleDir,
17251
+ setDir: task.set,
17252
+ title: manifest !== void 0 ? manifest.name : path50.basename(opts.bundleDir),
17253
+ repCandidates: statuses.map((s) => s.rep),
17254
+ report: { configs: statuses, behaviors, verdict: verdictWord, targetBar: opts.bar, verdictCaveats }
17255
+ });
17256
+ return { built: true, crops: built.crops };
17257
+ } catch (error) {
17258
+ return { built: false, error: error instanceof Error ? error.message.split("\n")[0] ?? String(error) : String(error) };
17259
+ }
17260
+ })();
16877
17261
  const report = {
16878
17262
  bundle: opts.bundleDir,
16879
17263
  scoredFiles,
@@ -16967,7 +17351,7 @@ async function runVerify(opts) {
16967
17351
  behaviors,
16968
17352
  evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
16969
17353
  composition: compositionBlock,
16970
- verdict: ok ? "verified" : "verification-failed",
17354
+ verdict: verdictWord,
16971
17355
  // Run-23 R5: the one-word verdict printed beside "coverage
16972
17356
  // denominator unknown" and "operability unverified" read, to a
16973
17357
  // JSON consumer who stops at `verdict`, as fully verified — the
@@ -16976,14 +17360,7 @@ async function runVerify(opts) {
16976
17360
  // consumer cannot claim it was never offered. Exit codes are
16977
17361
  // unchanged, deliberately: each claim in the report is true; the
16978
17362
  // caveats say which questions were never answered.
16979
- verdictCaveats: verdictCaveatsFor({
16980
- latticeBlock,
16981
- operability: coverage.operability,
16982
- compositionUnavailable: "unavailable" in compositionBlock,
16983
- fontsSubstituted: substitutedFamilies.length > 0,
16984
- pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
16985
- hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
16986
- }),
17363
+ verdictCaveats,
16987
17364
  // Machine-readable cause (adversarial review): a cert-bar failure
16988
17365
  // with zero pixel/behavior/composition failures was only
16989
17366
  // explainable from stderr prose.
@@ -16996,7 +17373,25 @@ async function runVerify(opts) {
16996
17373
  motion: motionDisclosure(opts.bundleDir, task.set),
16997
17374
  ...recordingSetDrift !== void 0 ? { recordingSetDrift } : {},
16998
17375
  ...substitutedFamilies.length > 0 ? { substitutedFamilies } : {},
16999
- eyeCheck: eyeCheck(opts.bundleDir)
17376
+ eyeCheck: eyeCheck(opts.bundleDir, sheetOutcome),
17377
+ // Field handoff 2026-08-26 (problem 2): a verified component's
17378
+ // value lands when it is LIVE — the terminal step of a green run
17379
+ // is publishing, and the report says so in the machine channel
17380
+ // too. Verify itself stays account-less and network-less
17381
+ // (ruler-authority invariant 3): this is a pointer, never a call.
17382
+ // Stdout-only guidance — verifyReportForTransport strips it, like
17383
+ // eyeCheck, before the report is persisted or uploaded.
17384
+ ...green ? {
17385
+ next: {
17386
+ action: "publish",
17387
+ command: tendrilCommand(`publish "${opts.bundleDir}"`),
17388
+ mcpTools: ["tendril_publish", "tendril_publish_wait (repeat while it returns approval-pending)"],
17389
+ note: "MCP hosts: use the tools, never the shell command \u2014 the split phases keep the approve link in hand while the wait polls. A FIRST publish waits for the user's Approve in their signed-in browser (the CLI cannot approve); a re-publish of an already-published component completes in one call"
17390
+ }
17391
+ } : {},
17392
+ // Stdout-only, like `next` — verifyReportForTransport strips it
17393
+ // (absolute host paths must never travel to a published page).
17394
+ ...composeScan !== void 0 ? { composeOpportunities: composeScan } : {}
17000
17395
  };
17001
17396
  emitData(opts, report, () => {
17002
17397
  for (const s of statuses) {
@@ -17128,7 +17523,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17128
17523
  }
17129
17524
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
17130
17525
  `);
17131
- const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path49.join(opts.bundleDir, f)).filter((f) => existsSync39(f)).map((f) => readFileSync35(f, "utf8")).join("\n")));
17526
+ const shippedFonts = requiredFontsManifest(cssFontFamilies(["styles.css", "tokens.css"].map((f) => path50.join(opts.bundleDir, f)).filter((f) => existsSync40(f)).map((f) => readFileSync36(f, "utf8")).join("\n")));
17132
17527
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
17133
17528
  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)
17134
17529
  `);
@@ -17141,8 +17536,58 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17141
17536
  }
17142
17537
  process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
17143
17538
  `);
17144
- process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)
17539
+ if (report.eyeCheck.sheetBuilt === true) {
17540
+ process.stdout.write(`eye check: sheet WRITTEN at ${report.eyeCheck.sheetPath} (${report.eyeCheck.sheetCrops ?? 0} detail crop pair(s)) \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape. Open it; ${report.eyeCheck.command} rebuilds it.
17541
+ `);
17542
+ } else {
17543
+ const buildNote = report.eyeCheck.sheetBuildError !== void 0 ? ` (this run tried to build it and could not: ${report.eyeCheck.sheetBuildError})` : "";
17544
+ process.stdout.write(`eye check: ${report.eyeCheck.command} \u2014 magnified recorded-vs-rendered crops of every small node; scores cannot see shape (re-run after every verify)${buildNote}
17545
+ `);
17546
+ }
17547
+ if (green) {
17548
+ process.stdout.write(
17549
+ `
17550
+ READY this component is verified \u2014 the run is not finished until it is LIVE: ${tendrilCommand(`publish "${opts.bundleDir}"`)} publishes it (a FIRST publish waits for the user's Approve in their signed-in browser \u2014 the CLI cannot approve; a re-publish completes in one call)
17551
+ `
17552
+ );
17553
+ }
17554
+ if (composeScan !== void 0) {
17555
+ const OPP_SHOWN = 5;
17556
+ if (composeScan.error !== void 0) {
17557
+ process.stdout.write(`COMPOSE discovery UNAVAILABLE (${composeScan.error}) \u2014 whether other recordings embed this component is UNKNOWN, not "no"
17558
+ `);
17559
+ }
17560
+ for (const o of composeScan.opportunities.slice(0, OPP_SHOWN)) {
17561
+ const hostQ = quoteArg(o.hostSet);
17562
+ if (o.kind === "confirmable") {
17563
+ process.stdout.write(
17564
+ `COMPOSE ${o.hostDisplayName} [${o.hostSet}] embeds this component in ${o.instances} instance(s) across ${o.hostReps.length} pose(s) \u2014 id-backed and confirmable. A human decides it, in their terminal, against the HOST set: ${tendrilCommand(`compose --set ${hostQ}`)} (decline instead with ${tendrilCommand(`compose --set ${hostQ} --decline ${o.pairKey ?? ""}`)}). After confirming, regenerate the HOST bundle (an agent run: brief \u2192 generate \u2192 score \u2192 verify, with --library pointing at the workspace holding this bundle) \u2014 composition never retrofits an existing bundle.
17565
+ `
17566
+ );
17567
+ } else if (o.kind === "ambiguous") {
17568
+ process.stdout.write(
17569
+ `COMPOSE ${o.hostDisplayName} [${o.hostSet}]: id evidence reaches this component AMBIGUOUSLY (${o.instances} instance(s)) \u2014 the confirm channel cannot take it yet; a human looks via ${tendrilCommand("compose --list")}
17570
+ `
17571
+ );
17572
+ } else {
17573
+ process.stdout.write(`COMPOSE ${o.hostDisplayName} [${o.hostSet}]: name-only match (${o.instances} instance(s)) \u2014 identity unproven, not confirmable. ${o.disclosures.find((d) => d.includes("re-record")) ?? "Id evidence is needed for a pairing."}
17574
+ `);
17575
+ }
17576
+ }
17577
+ if (composeScan.opportunities.length > OPP_SHOWN) {
17578
+ process.stdout.write(`COMPOSE \u2026 and ${composeScan.opportunities.length - OPP_SHOWN} more \u2014 ${tendrilCommand("compose --list")} shows the full report
17579
+ `);
17580
+ }
17581
+ for (const u of composeScan.hostsWithUnreadableDecisions) {
17582
+ process.stdout.write(`COMPOSE NOTE: ${u.hostSet} has ${u.unreadable} unreadable standing composition entr${u.unreadable === 1 ? "y" : "ies"} \u2014 decisions may exist that this scan cannot see
17145
17583
  `);
17584
+ }
17585
+ process.stdout.write(
17586
+ composeScan.skipped !== void 0 ? `COMPOSE scanned: (none) \u2014 the set's parent (${composeScan.skipped.dir}) holds ${composeScan.skipped.entries} entries and was not scanned as a recordings library; pass --library <dir> to scan one deliberately
17587
+ ` : `COMPOSE scanned: ${composeScan.scannedRoots.join(", ")} \u2014 recording sets elsewhere are NOT seen; pass --library <dir> to scan another workspace
17588
+ `
17589
+ );
17590
+ }
17146
17591
  });
17147
17592
  if (opts.bar === "cert" && substitutedFamilies.length > 0) {
17148
17593
  const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
@@ -17183,7 +17628,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17183
17628
  persistReport(opts, report, evidenceDir);
17184
17629
  }
17185
17630
  function persistReport(opts, report, evidenceDir) {
17186
- if (!existsSync39(evidenceDir)) return;
17631
+ if (!existsSync40(evidenceDir)) return;
17187
17632
  const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
17188
17633
  const withExit = {
17189
17634
  ...report,
@@ -17191,9 +17636,9 @@ function persistReport(opts, report, evidenceDir) {
17191
17636
  ...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
17192
17637
  };
17193
17638
  try {
17194
- writeFileSync17(
17195
- path49.join(evidenceDir, VERIFY_REPORT_FILENAME),
17196
- `${JSON.stringify(verifyReportForTransport(withExit, path49.basename(opts.bundleDir)), null, 2)}
17639
+ writeFileSync18(
17640
+ path50.join(evidenceDir, VERIFY_REPORT_FILENAME),
17641
+ `${JSON.stringify(verifyReportForTransport(withExit, path50.basename(opts.bundleDir)), null, 2)}
17197
17642
  `
17198
17643
  );
17199
17644
  } catch (e) {
@@ -17211,6 +17656,8 @@ var init_verify = __esm({
17211
17656
  init_src5();
17212
17657
  init_environment();
17213
17658
  init_font_guidance();
17659
+ init_compose2();
17660
+ init_inspect();
17214
17661
  init_invocation();
17215
17662
  init_output();
17216
17663
  init_profile_input();
@@ -17243,17 +17690,17 @@ __export(engine_exports, {
17243
17690
  runEngineBrief: () => runEngineBrief,
17244
17691
  runEngineScore: () => runEngineScore
17245
17692
  });
17246
- import { appendFileSync, existsSync as existsSync40, mkdirSync as mkdirSync12, readFileSync as readFileSync36, writeFileSync as writeFileSync18 } from "node:fs";
17247
- import path50 from "node:path";
17693
+ import { appendFileSync, existsSync as existsSync41, mkdirSync as mkdirSync12, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
17694
+ import path51 from "node:path";
17248
17695
  function resolveEngineTask(opts, callerCwd) {
17249
- const asPath = path50.resolve(callerCwd, opts.taskOrSet);
17250
- const isSet = existsSync40(path50.join(asPath, "recording-set.json"));
17696
+ const asPath = path51.resolve(callerCwd, opts.taskOrSet);
17697
+ const isSet = existsSync41(path51.join(asPath, "recording-set.json"));
17251
17698
  const registry = TASKS[opts.taskOrSet];
17252
17699
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
17253
17700
  if (isSet) {
17254
17701
  const manifest = loadManifest(asPath);
17255
17702
  const missing = manifest.reps.filter(
17256
- (r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync40(path50.join(asPath, r.slug, "get_design_context.json"))
17703
+ (r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync41(path51.join(asPath, r.slug, "get_design_context.json"))
17257
17704
  );
17258
17705
  if (missing.length > 0) {
17259
17706
  fail(opts, ExitCode.RecordingIncomplete, {
@@ -17267,7 +17714,7 @@ function resolveEngineTask(opts, callerCwd) {
17267
17714
  for (const d of authored.disclosures) warn(opts, d);
17268
17715
  return {
17269
17716
  task: authored.task,
17270
- name: path50.basename(asPath),
17717
+ name: path51.basename(asPath),
17271
17718
  ref: asPath,
17272
17719
  disclosures: authored.disclosures,
17273
17720
  interactionEvidence: authored.api.interactionEvidence,
@@ -17296,9 +17743,9 @@ function runEngineBrief(opts) {
17296
17743
  const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
17297
17744
  void reportRunPresence(name, "implementing");
17298
17745
  const bar = BARS3[opts.bar];
17299
- if (existsSync40(path50.join(task.set, "recording-set.json"))) {
17746
+ if (existsSync41(path51.join(task.set, "recording-set.json"))) {
17300
17747
  try {
17301
- const { open, skippedParent } = compositionPairsFor(path50.resolve(task.set), [opts.library !== void 0 ? path50.resolve(callerCwd, opts.library) : callerCwd]);
17748
+ const { open, proposals, skippedParent } = compositionPairsFor(path51.resolve(task.set), [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
17302
17749
  if (skippedParent !== void 0) {
17303
17750
  disclosures.push(
17304
17751
  `COMPOSITION DISCOVERY PARTIAL: the set's parent directory (${skippedParent.dir}) holds ${String(skippedParent.entries)} entries and was not scanned as a recordings library \u2014 sibling sets there are invisible to pairing. Pass --library <dir> to scan a specific library deliberately.`
@@ -17307,7 +17754,12 @@ function runEngineBrief(opts) {
17307
17754
  if (open.length > 0) {
17308
17755
  const componentNames = [...new Set(open.map((p) => p.displayName))];
17309
17756
  disclosures.push(
17310
- `COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path50.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
17757
+ `COMPOSITION PAIRS UNDECIDED: this recording references ${componentNames.length} other recorded component(s) (${open.map((p) => `${p.displayName} [pair-key ${p.key}]: ${p.instances.length} instance(s)`).join("; ")}) and no human has decided the pairing. This brief treats the component as SELF-CONTAINED \u2014 you will re-implement the partner's pixels locally. If composition is wanted: generate the partner bundle first, have a human run \`${tendrilCommand(`compose --set ${path51.resolve(task.set)}`)}\` in their terminal, and re-emit this brief. Proceeding as-is is valid; it just is not composition.`
17758
+ );
17759
+ }
17760
+ if (proposals.length > 0) {
17761
+ disclosures.push(
17762
+ `COMPOSITION NAME-ONLY: instance names match ${proposals.length} recorded component(s) (${proposals.map((p) => `${p.displayName}: ${p.instances} instance(s)`).join("; ")}) but identity is UNPROVEN \u2014 name evidence never auto-joins, so these regions are implemented locally. ${proposals.flatMap((p) => p.disclosures).find((d) => d.includes("re-record")) ?? "Record id evidence to make a pairing confirmable, or proceed self-contained."}`
17311
17763
  );
17312
17764
  }
17313
17765
  } catch (err) {
@@ -17324,9 +17776,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
17324
17776
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
17325
17777
  const segments = buildSegments(task, "files");
17326
17778
  let notRecorded;
17327
- const manifestPath2 = path50.join(task.set, "recording-set.json");
17328
- if (existsSync40(manifestPath2)) {
17329
- notRecorded = JSON.parse(readFileSync36(manifestPath2, "utf8")).notRecorded;
17779
+ const manifestPath2 = path51.join(task.set, "recording-set.json");
17780
+ if (existsSync41(manifestPath2)) {
17781
+ notRecorded = JSON.parse(readFileSync37(manifestPath2, "utf8")).notRecorded;
17330
17782
  }
17331
17783
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
17332
17784
 
@@ -17334,7 +17786,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
17334
17786
  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.
17335
17787
  ${notRecorded}` : "";
17336
17788
  let fontProvisioning;
17337
- if (existsSync40(manifestPath2)) {
17789
+ if (existsSync41(manifestPath2)) {
17338
17790
  const missingFams = unprovisionedFamilies(task.set);
17339
17791
  const unprovided = unprovisionedFaces(task.set);
17340
17792
  const weightOnly = missingFams.length === 0;
@@ -17356,7 +17808,7 @@ ${notRecorded}` : "";
17356
17808
  };
17357
17809
  }
17358
17810
  }
17359
- const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path50.resolve(callerCwd, opts.library) : callerCwd]);
17811
+ const pinsResult = composedPins(task.set, [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
17360
17812
  const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
17361
17813
  const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
17362
17814
 
@@ -17392,10 +17844,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
17392
17844
 
17393
17845
  === TASK PAYLOAD (recorded truth, verbatim) ===
17394
17846
  ${segments}`;
17395
- const payloadFile = path50.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
17396
- const candidateDirSuggestion = path50.resolve(callerCwd, `tendril-out/${name}-candidate`);
17397
- mkdirSync12(path50.dirname(payloadFile), { recursive: true });
17398
- writeFileSync18(payloadFile, payload);
17847
+ const payloadFile = path51.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
17848
+ const candidateDirSuggestion = path51.resolve(callerCwd, `tendril-out/${name}-candidate`);
17849
+ mkdirSync12(path51.dirname(payloadFile), { recursive: true });
17850
+ writeFileSync19(payloadFile, payload);
17399
17851
  emitData(
17400
17852
  opts,
17401
17853
  {
@@ -17441,7 +17893,7 @@ ${segments}`;
17441
17893
  // command must search the same bundle roots the pins came
17442
17894
  // from, or the oracle and the brief describe different worlds.
17443
17895
  `Run \`${tendrilCommand(
17444
- `engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path50.resolve(callerCwd, opts.library))}` : ""} --json`
17896
+ `engine score ${quoteArg(ref)} ${quoteArg(candidateDirSuggestion)} --bar ${opts.bar} --model ${opts.model ?? "<declared-model>"}${opts.library !== void 0 ? ` --library ${quoteArg(path51.resolve(callerCwd, opts.library))}` : ""} --json`
17445
17897
  )}\` \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.`,
17446
17898
  "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).",
17447
17899
  "Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
@@ -17456,8 +17908,8 @@ ${segments}`;
17456
17908
  );
17457
17909
  }
17458
17910
  function appendScoreHistory(candidateDir, entry) {
17459
- const file = path50.join(candidateDir, "score-history.jsonl");
17460
- const starts = existsSync40(file) ? readFileSync36(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
17911
+ const file = path51.join(candidateDir, "score-history.jsonl");
17912
+ const starts = existsSync41(file) ? readFileSync37(file, "utf8").split("\n").filter((l) => l.includes('"event":"round-start"')).length : 0;
17461
17913
  const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
17462
17914
  appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
17463
17915
  `);
@@ -17465,10 +17917,10 @@ function appendScoreHistory(candidateDir, entry) {
17465
17917
  async function runEngineScore(opts) {
17466
17918
  requireEntitlement(opts);
17467
17919
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
17468
- const candidateDir = path50.resolve(callerCwd, opts.candidateDir);
17920
+ const candidateDir = path51.resolve(callerCwd, opts.candidateDir);
17469
17921
  const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
17470
17922
  void reportRunPresence(name, "implementing");
17471
- if (!existsSync40(candidateDir)) {
17923
+ if (!existsSync41(candidateDir)) {
17472
17924
  fail(opts, ExitCode.InputValidation, {
17473
17925
  error: `candidate directory not found: ${candidateDir}`,
17474
17926
  code: "candidate-missing",
@@ -17493,10 +17945,10 @@ async function runEngineScore(opts) {
17493
17945
  for (const g of missingWeights(task.set)) {
17494
17946
  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)`);
17495
17947
  }
17496
- if (opts.rebind !== true && existsSync40(path50.join(candidateDir, "component.json"))) {
17948
+ if (opts.rebind !== true && existsSync41(path51.join(candidateDir, "component.json"))) {
17497
17949
  const prior = (() => {
17498
17950
  try {
17499
- const read = readBundleManifest(readFileSync36(path50.join(candidateDir, "component.json"), "utf8"));
17951
+ const read = readBundleManifest(readFileSync37(path51.join(candidateDir, "component.json"), "utf8"));
17500
17952
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
17501
17953
  } catch {
17502
17954
  return { unreadable: true };
@@ -17518,13 +17970,13 @@ async function runEngineScore(opts) {
17518
17970
  }
17519
17971
  }
17520
17972
  const bar = BARS3[opts.bar];
17521
- const evidenceDir = path50.join(candidateDir, "verify-evidence");
17973
+ const evidenceDir = path51.join(candidateDir, "verify-evidence");
17522
17974
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
17523
17975
  const hoverOpts = opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {};
17524
17976
  const parity = await checkHoverParity(task, candidateDir, task.configs, { ...hoverOpts, failureShotDir: evidenceDir });
17525
17977
  const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
17526
17978
  const occlusionFailures = occlusionRows.filter((o) => !o.pass);
17527
- const scorePins = composedPins(task.set, [opts.library !== void 0 ? path50.resolve(callerCwd, opts.library) : callerCwd]);
17979
+ const scorePins = composedPins(task.set, [opts.library !== void 0 ? path51.resolve(callerCwd, opts.library) : callerCwd]);
17528
17980
  const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
17529
17981
  const behaviors = [...await checkBehaviors(task, candidateDir, hoverOpts), ...parity, ...composition];
17530
17982
  const parityCoverage = parity.length > 0 ? `${parity.filter((p) => p.pass).length}/${parity.length} hover-forced configs` : "not applicable (no hover-forced configs in this set)";
@@ -17662,7 +18114,18 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
17662
18114
  // bundles verify then FAILED on the interaction-evidence gate —
17663
18115
  // the oracle must say what verify will say, including this.
17664
18116
  ...evidenceUnverified ? { interactionEvidenceUnverified: true, verifyWillFail: "interaction-evidence \u2014 the recording proves interactive poses none of the authored behaviors cover; not fixable from component code; REPORT it, do not iterate on it" } : {},
17665
- certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's A1.4 structural/crop checks can demote further" }
18117
+ certification: { certified: certifiedReps.length, total: scores.length, bar: certBar, note: "tierOf on exact values + parity and absent-ink demotion; verify's A1.4 structural/crop checks can demote further" },
18118
+ // Field handoff 2026-08-26 (problem 2): the loop's green is not
18119
+ // the pipeline's end — a run that stops at allPass leaves the
18120
+ // component reachable only on local disk. The oracle names the
18121
+ // remaining steps so no surface has to remember them for it.
18122
+ ...allPass ? {
18123
+ next: {
18124
+ action: "verify",
18125
+ command: tendrilCommand(`verify "${opts.candidateDir}" --bar ${certifiedReps.length === scores.length ? "cert" : "pass"}`),
18126
+ note: "allPass ends the LOOP, not the run: recompute with tendril_verify (the ruler's report + evidence), and a green verify's terminal step is publishing \u2014 the component is done when it is live, not when it scores"
18127
+ }
18128
+ } : {}
17666
18129
  },
17667
18130
  () => {
17668
18131
  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}]` : ""}
@@ -17744,11 +18207,11 @@ var codeconnect_exports = {};
17744
18207
  __export(codeconnect_exports, {
17745
18208
  runCodeConnect: () => runCodeConnect
17746
18209
  });
17747
- import { existsSync as existsSync41, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
17748
- import path51 from "node:path";
18210
+ import { existsSync as existsSync42, readFileSync as readFileSync38, writeFileSync as writeFileSync20 } from "node:fs";
18211
+ import path52 from "node:path";
17749
18212
  function runCodeConnect(opts) {
17750
18213
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
17751
- const bundleDir = path51.resolve(callerCwd, opts.bundleDir);
18214
+ const bundleDir = path52.resolve(callerCwd, opts.bundleDir);
17752
18215
  let url;
17753
18216
  try {
17754
18217
  url = new URL(opts.figmaUrl);
@@ -17764,7 +18227,7 @@ function runCodeConnect(opts) {
17764
18227
  }
17765
18228
  let manifest;
17766
18229
  try {
17767
- const read = readBundleManifest(readFileSync37(path51.join(bundleDir, "component.json"), "utf8"));
18230
+ const read = readBundleManifest(readFileSync38(path52.join(bundleDir, "component.json"), "utf8"));
17768
18231
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
17769
18232
  manifest = read.manifest;
17770
18233
  } catch (err) {
@@ -17774,8 +18237,8 @@ function runCodeConnect(opts) {
17774
18237
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
17775
18238
  });
17776
18239
  }
17777
- const setDir = path51.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
17778
- if (!existsSync41(path51.join(setDir, "recording-set.json"))) {
18240
+ const setDir = path52.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
18241
+ if (!existsSync42(path52.join(setDir, "recording-set.json"))) {
17779
18242
  fail(opts, ExitCode.InputValidation, {
17780
18243
  error: `recording set not found at ${setDir}`,
17781
18244
  code: "codeconnect-no-set",
@@ -17797,9 +18260,9 @@ function runCodeConnect(opts) {
17797
18260
  const recManifest = loadManifest(setDir);
17798
18261
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
17799
18262
  const meta = resolveRepEnvelopePath(setDir, r.slug, "metadata");
17800
- if (!existsSync41(meta)) return void 0;
18263
+ if (!existsSync42(meta)) return void 0;
17801
18264
  try {
17802
- return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync37(meta, "utf8"))))?.[1];
18265
+ return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync38(meta, "utf8"))))?.[1];
17803
18266
  } catch {
17804
18267
  return void 0;
17805
18268
  }
@@ -17864,7 +18327,7 @@ function runCodeConnect(opts) {
17864
18327
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
17865
18328
  fragmentVars.push(varName);
17866
18329
  }
17867
- const entryRel = path51.relative(callerCwd, path51.join(bundleDir, manifest.entry));
18330
+ const entryRel = path52.relative(callerCwd, path52.join(bundleDir, manifest.entry));
17868
18331
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
17869
18332
  const lines = [
17870
18333
  `// url=${opts.figmaUrl}`,
@@ -17888,8 +18351,8 @@ function runCodeConnect(opts) {
17888
18351
  `}`,
17889
18352
  ``
17890
18353
  ].join("\n");
17891
- const outFile = path51.resolve(callerCwd, opts.out ?? path51.join(bundleDir, `${component}.figma.ts`));
17892
- writeFileSync19(outFile, lines);
18354
+ const outFile = path52.resolve(callerCwd, opts.out ?? path52.join(bundleDir, `${component}.figma.ts`));
18355
+ writeFileSync20(outFile, lines);
17893
18356
  emitData(
17894
18357
  opts,
17895
18358
  {
@@ -17929,17 +18392,17 @@ var init_codeconnect = __esm({
17929
18392
 
17930
18393
  // packages/mcp/src/server.ts
17931
18394
  import { createHash as createHash11 } from "node:crypto";
17932
- import { existsSync as existsSync42, mkdtempSync as mkdtempSync3, readFileSync as readFileSync38, readdirSync as readdirSync16, writeFileSync as writeFileSync20 } from "node:fs";
18395
+ import { existsSync as existsSync43, mkdtempSync as mkdtempSync3, readFileSync as readFileSync39, readdirSync as readdirSync17, writeFileSync as writeFileSync21 } from "node:fs";
17933
18396
  import os8 from "node:os";
17934
- import path52 from "node:path";
18397
+ import path53 from "node:path";
17935
18398
  import { fileURLToPath as fileURLToPath6 } from "node:url";
17936
18399
  import { z as z15 } from "zod";
17937
18400
  function sourceHash() {
17938
- const dir = path52.dirname(fileURLToPath6(import.meta.url));
18401
+ const dir = path53.dirname(fileURLToPath6(import.meta.url));
17939
18402
  const h = createHash11("sha256");
17940
- for (const f of readdirSync16(dir).filter((n) => n.endsWith(".ts")).sort()) {
18403
+ for (const f of readdirSync17(dir).filter((n) => n.endsWith(".ts")).sort()) {
17941
18404
  h.update(f);
17942
- h.update(readFileSync38(path52.join(dir, f)));
18405
+ h.update(readFileSync39(path53.join(dir, f)));
17943
18406
  }
17944
18407
  return h.digest("hex").slice(0, 16);
17945
18408
  }
@@ -17947,10 +18410,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
17947
18410
  var init_server = __esm({
17948
18411
  "packages/mcp/src/server.ts"() {
17949
18412
  "use strict";
17950
- REPO_ROOT3 = path52.resolve(path52.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
17951
- CLI_BIN = path52.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
17952
- BUNDLED_CLI = path52.join(path52.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
17953
- CLI_SPAWN = existsSync42(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
18413
+ REPO_ROOT3 = path53.resolve(path53.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
18414
+ CLI_BIN = path53.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
18415
+ BUNDLED_CLI = path53.join(path53.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
18416
+ CLI_SPAWN = existsSync43(BUNDLED_CLI) ? { cmd: process.execPath, prefix: [BUNDLED_CLI] } : { cmd: "npx", prefix: ["tsx", CLI_BIN] };
17954
18417
  str = (d) => z15.string().describe(d);
17955
18418
  optStr = (d) => z15.string().optional().describe(d);
17956
18419
  TOOLS = [
@@ -17981,13 +18444,13 @@ var init_server = __esm({
17981
18444
  const single = i["metadata"];
17982
18445
  const parts = i["metadataParts"];
17983
18446
  if (single !== void 0 || parts !== void 0) {
17984
- const tmp = path52.join(mkdtempSync3(path52.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
18447
+ const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
17985
18448
  if (single !== void 0) {
17986
- writeFileSync20(tmp, single);
18449
+ writeFileSync21(tmp, single);
17987
18450
  argvOut.push("--metadata-raw-file", tmp);
17988
18451
  } else {
17989
18452
  if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
17990
- writeFileSync20(tmp, JSON.stringify(parts));
18453
+ writeFileSync21(tmp, JSON.stringify(parts));
17991
18454
  argvOut.push("--metadata-raw-parts-file", tmp);
17992
18455
  }
17993
18456
  }
@@ -18062,12 +18525,12 @@ var init_server = __esm({
18062
18525
  },
18063
18526
  {
18064
18527
  name: "tendril_publish_wait",
18065
- description: "Phase two of the browser-approved publish: waits (with live progress) for the user's Approve click on the page tendril_publish returned, then uploads the bundle, commits it, and returns the live publication URL. Call it right after relaying the approve link. A denial, a lapse, and success each come back as their own sentence \u2014 report the outcome and the URL to the user.",
18528
+ description: 'Phase two of the browser-approved publish. Waits in a BOUNDED window (~1 minute per call) for the user\'s Approve click on the page tendril_publish returned, then uploads the bundle, commits it, and returns the live publication URL. Call it right after relaying the approve link. While the human has not decided, each call returns `status: "approval-pending"` \u2014 that is a heartbeat, not a failure: tell the user in one short line that you are still waiting (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then call this again to keep waiting. The request stays live for ~30 minutes. A denial, a lapse, and success each come back as their own sentence \u2014 report the outcome, and on success lead with the live URL. A decided approval continues straight into upload and commit INSIDE the same call \u2014 that phase can take minutes and may render no progress in some hosts; that is normal, not stuck. Some hosts render no progress at all during a call; the bounded window IS the liveness, so never describe an in-flight call as stuck \u2014 and a brief liveness line roughly every few minutes is enough, you need not narrate every heartbeat (each pending return carries waitedTotalSeconds/remainingSeconds to say where the wait stands).',
18066
18529
  schema: z15.object({
18067
18530
  bundleDir: str("the same bundle directory tendril_publish was called with"),
18068
18531
  portal: optStr("portal origin override (must match tendril_publish's)")
18069
18532
  }),
18070
- argv: (i) => ["publish", i["bundleDir"], "--approve-wait", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
18533
+ argv: (i) => ["publish", i["bundleDir"], "--approve-wait", "--wait-window", "55", ...typeof i["portal"] === "string" ? ["--to", i["portal"]] : []]
18071
18534
  },
18072
18535
  {
18073
18536
  name: "tendril_record_next",
@@ -18116,14 +18579,14 @@ var init_server = __esm({
18116
18579
  const bridge = (label, single, parts) => {
18117
18580
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
18118
18581
  if (single === void 0 && parts === void 0) return;
18119
- const tmp = path52.join(mkdtempSync3(path52.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
18582
+ const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
18120
18583
  if (single !== void 0) {
18121
- writeFileSync20(tmp, single);
18584
+ writeFileSync21(tmp, single);
18122
18585
  argvOut.push(`--${label}-file`, tmp);
18123
18586
  } else {
18124
18587
  const blocks = parts;
18125
18588
  if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
18126
- writeFileSync20(tmp, JSON.stringify(blocks));
18589
+ writeFileSync21(tmp, JSON.stringify(blocks));
18127
18590
  argvOut.push(`--${label}-parts-file`, tmp);
18128
18591
  }
18129
18592
  };
@@ -18164,12 +18627,12 @@ var init_server = __esm({
18164
18627
  const file = i["file"];
18165
18628
  if ([text, texts, file].filter((x) => x !== void 0).length !== 1) throw new Error("pass exactly one of `text` (single-block response), `texts` (multi-block response), or `file` (a saved envelope)");
18166
18629
  if (file !== void 0) return [...base, "--file", file];
18167
- const tmp = path52.join(mkdtempSync3(path52.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
18630
+ const tmp = path53.join(mkdtempSync3(path53.join(os8.tmpdir(), "tendril-envelope-")), "response.txt");
18168
18631
  if (text !== void 0) {
18169
- writeFileSync20(tmp, text);
18632
+ writeFileSync21(tmp, text);
18170
18633
  return [...base, "--file", tmp, "--raw"];
18171
18634
  }
18172
- writeFileSync20(tmp, JSON.stringify(texts));
18635
+ writeFileSync21(tmp, JSON.stringify(texts));
18173
18636
  return [...base, "--file", tmp, "--raw-parts"];
18174
18637
  }
18175
18638
  },
@@ -18229,7 +18692,7 @@ var init_server = __esm({
18229
18692
  },
18230
18693
  {
18231
18694
  name: "tendril_engine_score",
18232
- description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself.",
18695
+ description: "AGENT-HARNESS engine, step 2 (the oracle): scores a candidate bundle directory against recorded truth \u2014 per-config pixels, behaviors, state parity (recording-selected \u2014 a bundle cannot unschedule it) \u2014 and returns feedback plus evidence artifacts. Iterate until allPass or two non-improving rounds. Only THIS tool's output counts as a score; never claim numbers yourself. allPass ends the LOOP, not the run: finish with tendril_verify, and a green verify's terminal step is publishing \u2014 the component is done when it is LIVE, not when it scores.",
18233
18696
  schema: z15.object({
18234
18697
  taskOrSet: str("reference task name or recording-set directory"),
18235
18698
  candidateDir: str("directory containing the proposed bundle files"),
@@ -18272,7 +18735,7 @@ var init_server = __esm({
18272
18735
  },
18273
18736
  {
18274
18737
  name: "tendril_verify",
18275
- 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.",
18738
+ 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. A GREEN run writes the inspect sheet itself and returns `next`: its terminal step is publishing (tendril_publish \u2192 tendril_publish_wait) \u2014 a verified component reachable only on local disk is an unfinished run; a sub-bar run never publishes.",
18276
18739
  schema: z15.object({
18277
18740
  bundleDir: str("bundle directory to verify"),
18278
18741
  bar: optStr("pass (default) or cert"),
@@ -18326,13 +18789,13 @@ __export(permissions_exports, {
18326
18789
  runPermissions: () => runPermissions,
18327
18790
  writeSelection: () => writeSelection
18328
18791
  });
18329
- import { existsSync as existsSync43, mkdirSync as mkdirSync13, readFileSync as readFileSync39, writeFileSync as writeFileSync21 } from "node:fs";
18792
+ import { existsSync as existsSync44, mkdirSync as mkdirSync13, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
18330
18793
  import os9 from "node:os";
18331
- import path53 from "node:path";
18794
+ import path54 from "node:path";
18332
18795
  function mergeAllowlist(file, entries, denyEntries = []) {
18333
18796
  let settings = {};
18334
- if (existsSync43(file) && readFileSync39(file, "utf8").trim() !== "") {
18335
- settings = JSON.parse(readFileSync39(file, "utf8"));
18797
+ if (existsSync44(file) && readFileSync40(file, "utf8").trim() !== "") {
18798
+ settings = JSON.parse(readFileSync40(file, "utf8"));
18336
18799
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
18337
18800
  }
18338
18801
  const permissions = settings["permissions"] ??= {};
@@ -18352,8 +18815,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
18352
18815
  }
18353
18816
  if (added.length > 0 || denyAdded.length > 0) {
18354
18817
  allow.push(...added);
18355
- mkdirSync13(path53.dirname(file), { recursive: true });
18356
- writeFileSync21(file, `${JSON.stringify(settings, null, 2)}
18818
+ mkdirSync13(path54.dirname(file), { recursive: true });
18819
+ writeFileSync22(file, `${JSON.stringify(settings, null, 2)}
18357
18820
  `);
18358
18821
  }
18359
18822
  return { added, alreadyPresent, denyAdded };
@@ -18417,7 +18880,7 @@ async function runPermissions(flags) {
18417
18880
  }
18418
18881
  if (flags.write) {
18419
18882
  const base = process.env["INIT_CWD"] ?? process.cwd();
18420
- const file = flags.user ? path53.join(os9.homedir(), ".claude", "settings.json") : path53.join(base, ".claude", "settings.local.json");
18883
+ const file = flags.user ? path54.join(os9.homedir(), ".claude", "settings.json") : path54.join(base, ".claude", "settings.local.json");
18421
18884
  const { entries, denyEntries } = writeSelection(result, flags.user === true);
18422
18885
  if (flags.dryRun) {
18423
18886
  emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
@@ -18560,168 +19023,6 @@ var init_permissions = __esm({
18560
19023
  }
18561
19024
  });
18562
19025
 
18563
- // packages/cli/src/commands/inspect.ts
18564
- var inspect_exports = {};
18565
- __export(inspect_exports, {
18566
- INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
18567
- runInspect: () => runInspect
18568
- });
18569
- import { existsSync as existsSync44, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
18570
- import path54 from "node:path";
18571
- function readVerifyReport(evidenceDir) {
18572
- const p = path54.join(evidenceDir, VERIFY_REPORT_FILENAME);
18573
- if (!existsSync44(p)) return void 0;
18574
- try {
18575
- return JSON.parse(readFileSync40(p, "utf8"));
18576
- } catch {
18577
- return void 0;
18578
- }
18579
- }
18580
- function verdictBlock(report) {
18581
- if (report === void 0) {
18582
- return `<div class="verdict none"><h2>No verdict on file</h2><p>This evidence carries no <code>${VERIFY_REPORT_FILENAME}</code>, so these images are unlabelled pixels \u2014 nothing here says what passed. Re-run <code>tendril verify</code> to write one.</p></div>`;
18583
- }
18584
- const buckets = /* @__PURE__ */ new Map();
18585
- for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
18586
- const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
18587
- const failed = report.behaviors.filter((b) => !b.pass);
18588
- return `<div class="verdict ${report.verdict === "verified" ? "ok" : "bad"}"><h2>${esc(report.verdict)} <small>at bar \u201C${esc(report.targetBar)}\u201D</small></h2><p class="tiers">${split}<span class="tier">${report.behaviors.length - failed.length}/${report.behaviors.length} behaviors</span></p>` + (failed.length > 0 ? `<ul class="fails">${failed.map((b) => `<li><code>${esc(b.id)}</code>${b.detail !== void 0 ? ` \u2014 ${esc(b.detail)}` : ""}</li>`).join("")}</ul>` : "") + (report.verdictCaveats.length > 0 ? `<h3>What was never asked</h3><ul class="caveats">${report.verdictCaveats.map((c) => `<li>${esc(c)}</li>`).join("")}</ul>` : `<p class="caveats">No caveats: every question this bar asks was answered.</p>`) + `</div>`;
18589
- }
18590
- function scoreLine(report, rep) {
18591
- const c = report?.configs.find((x) => x.rep === rep);
18592
- if (c === void 0) return "";
18593
- const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
18594
- const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
18595
- const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
18596
- return `<p class="scores"><span class="tier t-${esc(c.status)}">${esc(c.status)}</span> sim ${c.similarity} \xB7 ink ${c.inkRecall}${ex} ${why} ${err}</p>`;
18597
- }
18598
- async function runInspect(opts) {
18599
- if (opts.describe) {
18600
- printDescription(INSPECT_DESCRIPTION);
18601
- return;
18602
- }
18603
- const bundleDir = path54.resolve(opts.bundleDir);
18604
- const evidenceDir = path54.join(bundleDir, "verify-evidence");
18605
- const manifestPath2 = path54.join(bundleDir, "component.json");
18606
- if (!existsSync44(evidenceDir) || !existsSync44(manifestPath2)) {
18607
- fail(opts, ExitCode.InputValidation, {
18608
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync44(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
18609
- code: "no-evidence",
18610
- remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
18611
- });
18612
- }
18613
- const { manifest } = readBundleManifest(readFileSync40(manifestPath2, "utf8"));
18614
- if (manifest === void 0) {
18615
- fail(opts, ExitCode.InputValidation, {
18616
- error: "component.json did not parse as a bundle manifest",
18617
- code: "no-mount-contract",
18618
- remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
18619
- });
18620
- }
18621
- const setDir = path54.resolve(opts.set ?? manifest.provenance.recordingSet.path);
18622
- const report = readVerifyReport(evidenceDir);
18623
- const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync44(path54.join(evidenceDir, `${rep}-ref.png`)) && existsSync44(path54.join(evidenceDir, `${rep}-render.png`)));
18624
- if (reps.length === 0) {
18625
- fail(opts, ExitCode.InputValidation, {
18626
- error: "verify-evidence holds no ref/render pairs for this bundle's configs",
18627
- code: "no-evidence",
18628
- remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
18629
- });
18630
- }
18631
- let crops = 0;
18632
- const sections = [];
18633
- for (const rep of reps) {
18634
- const ref = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-ref.png`)));
18635
- const render = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-render.png`)));
18636
- const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
18637
- const cells = [];
18638
- for (const [i, n] of nodes.entries()) {
18639
- const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
18640
- try {
18641
- writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
18642
- writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
18643
- } catch {
18644
- continue;
18645
- }
18646
- crops += 1;
18647
- cells.push(
18648
- `<figure><figcaption>${esc(n.name)} <code>${esc(n.id)}</code> \xB7 ${n.w}\xD7${n.h}</figcaption><div class="pair"><span><em>recorded</em><img src="./${rep}-inspect-${i}-ref.png" alt="recorded ${esc(n.name)}"></span><span><em>rendered</em><img src="./${rep}-inspect-${i}-render.png" alt="rendered ${esc(n.name)}"></span></div></figure>`
18649
- );
18650
- }
18651
- sections.push(
18652
- `<section><h2>${esc(rep)}</h2>` + scoreLine(report, rep) + `<div class="full"><span><em>recorded</em><img src="./${rep}-ref.png"></span><span><em>rendered</em><img src="./${rep}-render.png"></span><span><em>diff</em><img src="./${rep}-diff.png"></span></div>` + (cells.length > 0 ? `<div class="grid">${cells.join("")}</div>` : `<p class="none">no small recorded nodes in this config's sweep</p>`) + `</section>`
18653
- );
18654
- }
18655
- for (const c of report?.configs ?? []) {
18656
- if (reps.includes(c.rep)) continue;
18657
- sections.push(`<section class="missing"><h2>${esc(c.rep)}</h2>${scoreLine(report, c.rep)}<p class="none">No evidence images for this config \u2014 it was scored, but nothing was captured to look at.</p></section>`);
18658
- }
18659
- const sheet = path54.join(evidenceDir, "inspect.html");
18660
- writeFileSync22(
18661
- sheet,
18662
- `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
18663
- body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
18664
- h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
18665
- .pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
18666
- .full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
18667
- figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
18668
- .grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
18669
- .verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
18670
- .verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
18671
- .verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
18672
- .verdict small{font-weight:400;color:#666}
18673
- .tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
18674
- .tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
18675
- .t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
18676
- .caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
18677
- .scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
18678
- .missing{opacity:.85} .legend{white-space:pre-wrap;font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;background:#fafafa;border:1px solid #ddd;padding:12px;overflow-x:auto}
18679
- </style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
18680
- ${verdictBlock(report)}
18681
- <p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
18682
- Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
18683
- whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
18684
- ${sections.join("\n")}
18685
- <section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
18686
- `
18687
- );
18688
- emitData(opts, { sheet, configs: reps.length, crops }, () => {
18689
- process.stdout.write(`inspect sheet: ${sheet}
18690
- ${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
18691
- `);
18692
- });
18693
- }
18694
- var INSPECT_DESCRIPTION, esc;
18695
- var init_inspect = __esm({
18696
- "packages/cli/src/commands/inspect.ts"() {
18697
- "use strict";
18698
- init_src3();
18699
- init_src4();
18700
- init_src5();
18701
- init_describe();
18702
- init_invocation();
18703
- init_output();
18704
- INSPECT_DESCRIPTION = {
18705
- name: "inspect",
18706
- summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
18707
- args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
18708
- flags: [
18709
- { flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
18710
- { flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
18711
- { flag: "--json", description: "Machine-readable output" }
18712
- ],
18713
- output: {
18714
- sheet: "string \u2014 path to the generated inspect.html",
18715
- configs: "number \u2014 configs with evidence found",
18716
- crops: "number \u2014 detail crop pairs written"
18717
- },
18718
- exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
18719
- examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
18720
- };
18721
- esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18722
- }
18723
- });
18724
-
18725
19026
  // packages/cli/src/commands/login.ts
18726
19027
  var login_exports = {};
18727
19028
  __export(login_exports, {
@@ -19297,12 +19598,28 @@ var init_share = __esm({
19297
19598
  // packages/cli/src/commands/publish.ts
19298
19599
  var publish_exports = {};
19299
19600
  __export(publish_exports, {
19601
+ approveWaitPhase: () => approveWaitPhase,
19300
19602
  resolveOrigin: () => resolveOrigin2,
19301
- runPublish: () => runPublish
19603
+ runPublish: () => runPublish,
19604
+ spendPendingApproval: () => spendPendingApproval
19302
19605
  });
19303
19606
  import { existsSync as existsSync47, readFileSync as readFileSync43, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
19304
19607
  import path57 from "node:path";
19305
19608
  async function runPublish(opts) {
19609
+ if (opts.waitWindowSeconds !== void 0 && opts.approveWait !== true) {
19610
+ fail(opts, ExitCode.InputValidation, {
19611
+ error: "--wait-window only bounds an --approve-wait poll",
19612
+ code: "wait-window-without-approve-wait",
19613
+ remediation: "Pass --approve-wait with it, or drop --wait-window."
19614
+ });
19615
+ }
19616
+ if (opts.waitWindowSeconds !== void 0 && (!Number.isFinite(opts.waitWindowSeconds) || opts.waitWindowSeconds <= 0)) {
19617
+ fail(opts, ExitCode.InputValidation, {
19618
+ error: "--wait-window needs a positive number of seconds",
19619
+ code: "wait-window-invalid",
19620
+ remediation: "Pass e.g. --wait-window 55."
19621
+ });
19622
+ }
19306
19623
  const bundleDir = path57.resolve(opts.bundleDir);
19307
19624
  const bundle = readBundle(opts, bundleDir);
19308
19625
  const report = bundle.report;
@@ -19416,9 +19733,11 @@ async function runPublish(opts) {
19416
19733
  const origin = resolveOrigin2(opts);
19417
19734
  const client = opts.client ?? httpClient(opts, origin);
19418
19735
  if (opts.approveWait === true) {
19419
- await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
19736
+ const phase = await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
19737
+ if (phase === "yielded") return;
19420
19738
  }
19421
19739
  void reportRunPresence(componentName, "publishing");
19740
+ emitProgress(0, 1, "requesting the upload plan from the portal");
19422
19741
  let opened = await client.begin({
19423
19742
  componentName,
19424
19743
  figmaFile,
@@ -19445,6 +19764,7 @@ async function runPublish(opts) {
19445
19764
  }
19446
19765
  refuse2(opts, opened, "publish-refused");
19447
19766
  }
19767
+ if (opts.approveWait === true) spendPendingApproval();
19448
19768
  const uploaded = [];
19449
19769
  for (const object of opened.value.plan.objects) {
19450
19770
  const file = path57.join(bundleDir, object.relPath);
@@ -19464,6 +19784,7 @@ async function runPublish(opts) {
19464
19784
  uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
19465
19785
  emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
19466
19786
  }
19787
+ emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
19467
19788
  const committed = await client.commit({ publicationId: opened.value.publicationId });
19468
19789
  if (committed.ok) {
19469
19790
  await endRunPresence(componentName);
@@ -19605,7 +19926,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
19605
19926
  const who = await client.whoami?.();
19606
19927
  const asAccount = who?.ok === true && who.value.email !== "" ? ` signed in as ${who.value.email}` : "";
19607
19928
  if (opts.approveStart === true) {
19608
- const pending = { ...approval, bundleDir, componentName: input.componentName, figmaFile: input.figmaFile };
19929
+ const pending = { ...approval, bundleDir, componentName: input.componentName, figmaFile: input.figmaFile, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
19609
19930
  writeFileSync25(pendingApprovalPath(), `${JSON.stringify(pending, null, 2)}
19610
19931
  `, { mode: 384 });
19611
19932
  await endRunPresence(input.componentName);
@@ -19638,7 +19959,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
19638
19959
  const decided = await waitForApproval(opts, client, approval);
19639
19960
  if (decided === "approved") return input.begin();
19640
19961
  await endRunPresence(input.componentName);
19641
- failDecision(opts, decided);
19962
+ failDecision(opts, decided === "window-elapsed" ? "expired" : decided);
19642
19963
  }
19643
19964
  async function approveWaitPhase(opts, client, bundleDir, subject) {
19644
19965
  const file = pendingApprovalPath();
@@ -19668,12 +19989,41 @@ async function approveWaitPhase(opts, client, bundleDir, subject) {
19668
19989
  }
19669
19990
  const done = () => rmSync10(file, { force: true });
19670
19991
  const decided = await waitForApproval(opts, client, pending);
19671
- done();
19672
- if (decided !== "approved") failDecision(opts, decided);
19992
+ if (decided === "window-elapsed") {
19993
+ const who = await client.whoami?.();
19994
+ const asAccount = who?.ok === true && who.value.email !== "" ? who.value.email : void 0;
19995
+ emitData(
19996
+ opts,
19997
+ {
19998
+ status: "approval-pending",
19999
+ approveUrl: pending.approveUrl,
20000
+ ...asAccount !== void 0 ? { approveAsAccount: asAccount } : {},
20001
+ expiresAt: pending.expiresAt,
20002
+ windowSeconds: opts.waitWindowSeconds ?? 0,
20003
+ ...pending.requestedAt !== void 0 ? { waitedTotalSeconds: Math.max(0, Math.round((Date.now() - Date.parse(pending.requestedAt)) / 1e3)) } : {},
20004
+ remainingSeconds: Math.max(0, Math.round((Date.parse(pending.expiresAt) - Date.now()) / 1e3)),
20005
+ next: "the approval request is STILL LIVE and undecided \u2014 tell the user in one short line you are still waiting for their Approve click (restate the approve link and the account to approve as, ONLY if they seem lost; never urge the decision), then run the wait again to keep waiting"
20006
+ },
20007
+ () => {
20008
+ process.stdout.write(`Still waiting for the browser approval (wait window elapsed; the request stays live until ${pending.expiresAt.slice(11, 16)} UTC): ${pending.approveUrl}
20009
+ `);
20010
+ }
20011
+ );
20012
+ return "yielded";
20013
+ }
20014
+ if (decided !== "approved") {
20015
+ done();
20016
+ failDecision(opts, decided);
20017
+ }
20018
+ return "proceed";
20019
+ }
20020
+ function spendPendingApproval() {
20021
+ rmSync10(pendingApprovalPath(), { force: true });
19673
20022
  }
19674
20023
  async function waitForApproval(opts, client, approval) {
19675
20024
  const interval = Math.max(1, approval.pollSeconds) * 1e3;
19676
- const total = Math.ceil(APPROVAL_WAIT_CAP_MS / interval);
20025
+ const capMs = opts.waitWindowSeconds !== void 0 ? Math.max(1, opts.waitWindowSeconds) * 1e3 : APPROVAL_WAIT_CAP_MS;
20026
+ const total = Math.ceil(capMs / interval);
19677
20027
  for (let tick = 1; tick <= total; tick += 1) {
19678
20028
  const polled = await client.pollApproval({ approvalId: approval.approvalId });
19679
20029
  if (!polled.ok) refuse2(opts, polled, "approval-poll-refused");
@@ -19681,7 +20031,7 @@ async function waitForApproval(opts, client, approval) {
19681
20031
  emitProgress(tick, total, "waiting for the browser approval");
19682
20032
  await new Promise((resolve) => setTimeout(resolve, interval));
19683
20033
  }
19684
- return "expired";
20034
+ return opts.waitWindowSeconds !== void 0 ? "window-elapsed" : "expired";
19685
20035
  }
19686
20036
  function failDecision(opts, decided) {
19687
20037
  if (decided === "denied") {
@@ -21195,7 +21545,7 @@ function buildProgram() {
21195
21545
  ...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
21196
21546
  });
21197
21547
  });
21198
- program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").action(async (bundleDir, _o, cmd) => {
21548
+ program.command("publish").description("Put a verified bundle and its evidence on a page, at a URL you can send to someone. Refuses a run the ruler declined.").argument("<bundleDir>", "bundle directory that has already been verified").option("--to <url>", "the portal to publish to (or set TENDRIL_PORTAL_URL)").option("--name <name>", "library label for this component (default: the bundle manifest's own name)").option("--approve-start", "first publish only: request the browser approval, print the link, persist the pending state and exit \u2014 the tendril_publish tool's phase one").option("--approve-wait", "resume a pending approval: poll until the human decides in the browser, then publish \u2014 phase two").option("--wait-window <seconds>", "with --approve-wait: return after this many undecided seconds (exit 0, status approval-pending, the pending slot kept) instead of blocking to the 31-minute cap \u2014 the MCP bridge's bounded-poll shape").action(async (bundleDir, _o, cmd) => {
21199
21549
  const flags = globalFlags(cmd.parent);
21200
21550
  const local = cmd.opts();
21201
21551
  const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
@@ -21205,7 +21555,10 @@ function buildProgram() {
21205
21555
  ...local["to"] !== void 0 ? { to: local["to"] } : {},
21206
21556
  ...local["name"] !== void 0 ? { name: local["name"] } : {},
21207
21557
  ...local["approveStart"] !== void 0 ? { approveStart: local["approveStart"] } : {},
21208
- ...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {}
21558
+ ...local["approveWait"] !== void 0 ? { approveWait: local["approveWait"] } : {},
21559
+ // Parsed, never filtered: a bad value must refuse loudly in
21560
+ // runPublish, not silently become the unbounded wait.
21561
+ ...local["waitWindow"] !== void 0 ? { waitWindowSeconds: Number.parseFloat(local["waitWindow"]) } : {}
21209
21562
  });
21210
21563
  });
21211
21564
  program.command("verify").description("Recompute verification for a generated bundle against its recording set (per-config status, behaviors, honest exit codes).").argument("<bundleDir>", "bundle directory to verify").option("--task <name>", "legacy task adapter for bundles WITHOUT component.json (bundle v1 carries its own prop manifest)").option("--set <dir>", "recording set directory (overrides the bundle's provenance path)").option("--bar <bar>", "target bar: pass (0.95) or cert (0.97)", "pass").option("--library <dir>", "workspace root holding confirmed partners generated bundles (ADR-013 composed pins; default: current directory)").option("--profile <file>", "a `tendril profile` artifact; reports whether the bundle followed your codebase conventions (never gates the verdict)").option("--hover-timeout <ms>", "hover actionability budget in ms (default 2000) \u2014 for diagnosing a slow machine; a non-default value is recorded in the report as a verdict caveat, never silently").action(async (bundleDir, _opts, cmd) => {