@tendrilapp/cli 0.1.47 → 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
@@ -842,9 +842,12 @@ import { existsSync } from "node:fs";
842
842
  import path from "node:path";
843
843
  import { z as z4 } from "zod";
844
844
  function resolveRepEnvelopePath(setDir, slug, kind) {
845
- const mcp = path.join(setDir, slug, kind === "metadata" ? "get_metadata.json" : "get_screenshot.json");
845
+ return resolveRepEnvelopePathIn(path.join(setDir, slug), kind);
846
+ }
847
+ function resolveRepEnvelopePathIn(repDir, kind) {
848
+ const mcp = path.join(repDir, kind === "metadata" ? "get_metadata.json" : "get_screenshot.json");
846
849
  if (existsSync(mcp)) return mcp;
847
- const rest = path.join(setDir, slug, kind === "metadata" ? REST_METADATA_FILE : REST_SCREENSHOT_FILE);
850
+ const rest = path.join(repDir, kind === "metadata" ? REST_METADATA_FILE : REST_SCREENSHOT_FILE);
848
851
  return existsSync(rest) ? rest : mcp;
849
852
  }
850
853
  function repEnvelopeExists(setDir, slug, kind) {
@@ -1670,7 +1673,7 @@ function effectivelyInvisible(node) {
1670
1673
  return false;
1671
1674
  }
1672
1675
  function repVisibility(setDir, slug) {
1673
- const file = path3.join(setDir, slug, "get_metadata.json");
1676
+ const file = resolveRepEnvelopePathIn(path3.join(setDir, slug), "metadata");
1674
1677
  if (!existsSync3(file)) return void 0;
1675
1678
  let roots;
1676
1679
  try {
@@ -1721,6 +1724,7 @@ var init_visibility = __esm({
1721
1724
  "packages/figma/src/recording/visibility.ts"() {
1722
1725
  "use strict";
1723
1726
  init_normalize();
1727
+ init_rest_envelopes();
1724
1728
  init_session();
1725
1729
  normalizedLayerName = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
1726
1730
  invisibleInEveryPose = (v) => v.presentIn.length > 0 && v.visibleIn.length === 0;
@@ -1778,7 +1782,7 @@ function buildComposeIndex(roots, depth = 3) {
1778
1782
  for (const rep of manifest.reps) {
1779
1783
  variantNodeIds.add(rep.nodeId);
1780
1784
  repSlugByVariantNode.set(rep.nodeId, rep.slug);
1781
- const metaFile = path4.join(dir, rep.slug, "get_metadata.json");
1785
+ const metaFile = resolveRepEnvelopePathIn(path4.join(dir, rep.slug), "metadata");
1782
1786
  if (!existsSync4(metaFile)) continue;
1783
1787
  try {
1784
1788
  const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
@@ -1837,8 +1841,31 @@ function emissionTails(setDir, repSlug) {
1837
1841
  }
1838
1842
  return byHead;
1839
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
+ }
1840
1867
  function hostInstances(setDir, repSlug) {
1841
- const metaFile = path4.join(setDir, repSlug, "get_metadata.json");
1868
+ const metaFile = resolveRepEnvelopePathIn(path4.join(setDir, repSlug), "metadata");
1842
1869
  if (!existsSync4(metaFile)) return [];
1843
1870
  try {
1844
1871
  const text = envelopeTextContent(JSON.parse(readFileSync3(metaFile, "utf8")));
@@ -1867,10 +1894,11 @@ function composeReport(index) {
1867
1894
  }
1868
1895
  }
1869
1896
  for (const [variantNodeId, slug] of host.repSlugByVariantNode) {
1870
- void variantNodeId;
1871
1897
  const instances = hostInstances(host.dir, slug);
1872
1898
  if (instances.length === 0) continue;
1873
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);
1874
1902
  for (const inst of instances) {
1875
1903
  if (visibleEver.get(inst.id) !== true) {
1876
1904
  edges.push({
@@ -1887,10 +1915,12 @@ function composeReport(index) {
1887
1915
  continue;
1888
1916
  }
1889
1917
  const tails = tailsByHead.get(inst.id) ?? /* @__PURE__ */ new Map();
1918
+ const cid = cidByInstance.get(inst.id);
1890
1919
  const disclosures = [];
1891
1920
  const refused = [];
1892
1921
  const idCands = index.filter((c) => {
1893
1922
  if (c.dir === host.dir || sameComponent(c, host)) return false;
1923
+ if (cid !== void 0 && c.variantNodeIds.has(cid)) return true;
1894
1924
  for (const t of tails.keys()) if (c.ownIds.has(t)) return true;
1895
1925
  return false;
1896
1926
  });
@@ -1938,7 +1968,15 @@ function composeReport(index) {
1938
1968
  partners: nameProps.map((p) => ({ dir: p.dir, displayName: p.displayName, ...p.figmaFile !== void 0 ? { figmaFile: p.figmaFile } : {} })),
1939
1969
  disclosures: [
1940
1970
  ...disclosures,
1941
- `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
+ ] : []
1942
1980
  ]
1943
1981
  });
1944
1982
  } else if (disclosures.length > 0) {
@@ -1961,8 +1999,10 @@ function composeReport(index) {
1961
1999
  const group = eligible[0];
1962
2000
  const ownedDepths = [];
1963
2001
  for (const [t, d] of tails) if (group.some((m) => m.ownIds.has(t))) ownedDepths.push(d);
1964
- 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);
1965
2004
  const poseVariants = /* @__PURE__ */ new Set();
2005
+ if (cidBacked) poseVariants.add(cid);
1966
2006
  for (const m of group) {
1967
2007
  for (const [t, d] of tails) {
1968
2008
  if (d !== 1 || !m.ownIds.has(t)) continue;
@@ -1973,6 +2013,9 @@ function composeReport(index) {
1973
2013
  }
1974
2014
  }
1975
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
+ }
1976
2019
  for (const p of nameProps) {
1977
2020
  if (!group.some((m) => sameComponent(m, p))) {
1978
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`);
@@ -2016,7 +2059,7 @@ function composeReport(index) {
2016
2059
  },
2017
2060
  disclosures: [
2018
2061
  ...disclosures,
2019
- "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)`
2020
2063
  ]
2021
2064
  });
2022
2065
  }
@@ -2104,7 +2147,7 @@ function confirmedCompositionStatus(hostSet) {
2104
2147
  status: ok ? stale ? "stale-supported" : "supported" : stale ? "stale-unsupported" : "unsupported",
2105
2148
  instances: entry.instances,
2106
2149
  affectedReps: ok ? [] : [...new Set(unsupported.map((i) => i.hostRep))],
2107
- 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" : ""}`
2108
2151
  });
2109
2152
  }
2110
2153
  return { rows, ...malformedEntries.length > 0 ? { malformed: malformedEntries.join("; ") } : {} };
@@ -2112,11 +2155,111 @@ function confirmedCompositionStatus(hostSet) {
2112
2155
  function createHashHex(bytes) {
2113
2156
  return createHash2("sha256").update(bytes).digest("hex");
2114
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
+ }
2115
2257
  var CompositionEntrySchema, norm, FOOTER, kitLabel, toPosixRel, fromStoredRel, pairKeyFor;
2116
2258
  var init_compose = __esm({
2117
2259
  "packages/figma/src/recording/compose.ts"() {
2118
2260
  "use strict";
2119
2261
  init_session();
2262
+ init_rest_envelopes();
2120
2263
  init_normalize();
2121
2264
  init_visibility();
2122
2265
  CompositionEntrySchema = z6.object({
@@ -6647,7 +6790,7 @@ var init_bundle_files = __esm({
6647
6790
  // packages/metadata/src/verify-report.ts
6648
6791
  function verifyReportForTransport(report, bundleName) {
6649
6792
  const { dir: _dir, ...evidence } = report.evidence;
6650
- const { eyeCheck: _eyeCheck, ...rest } = report;
6793
+ const { eyeCheck: _eyeCheck, next: _next, composeOpportunities: _composeOpportunities, ...rest } = report;
6651
6794
  return { ...rest, bundle: bundleName, evidence };
6652
6795
  }
6653
6796
  function nonEmptyArray(value) {
@@ -11631,7 +11774,8 @@ var init_brief = __esm({
11631
11774
  import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync10 } from "node:fs";
11632
11775
  import path36 from "node:path";
11633
11776
  function repText(set, rep, tool) {
11634
- const env = JSON.parse(readFileSync26(path36.join(set, rep, `${tool}.json`), "utf8"));
11777
+ const file = tool === "get_metadata" ? resolveRepEnvelopePath(set, rep, "metadata") : tool === "get_screenshot" ? resolveRepEnvelopePath(set, rep, "screenshot") : path36.join(set, rep, `${tool}.json`);
11778
+ const env = JSON.parse(readFileSync26(file, "utf8"));
11635
11779
  return tool === "get_design_context" ? envelopeFirstTextPart(env) : envelopeTextContent(env);
11636
11780
  }
11637
11781
  function refPngDims(set, rep) {
@@ -11791,7 +11935,6 @@ var init_segments = __esm({
11791
11935
  "packages/generate/src/segments.ts"() {
11792
11936
  "use strict";
11793
11937
  init_src();
11794
- init_src();
11795
11938
  cssIdent = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
11796
11939
  }
11797
11940
  });
@@ -14434,7 +14577,16 @@ function compositionPairsFor(hostSet, roots) {
14434
14577
  else invalid++;
14435
14578
  }
14436
14579
  const decidedKeys = new Set(standing.map((c) => fromStoredRel(c.partner.key)));
14437
- 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 } : {} };
14438
14590
  }
14439
14591
  function substitutionPairs(edges, hostSet) {
14440
14592
  const pairs = /* @__PURE__ */ new Map();
@@ -14879,7 +15031,7 @@ async function runRecordPlan(opts) {
14879
15031
  ...opts.figmaFile !== void 0 ? { figmaFile: opts.figmaFile } : {},
14880
15032
  ...setIdentity
14881
15033
  });
14882
- const channelNote = await assignRestChannel(opts.setDir, manifest, resumed);
15034
+ const channelNote = await assignRestChannel(opts.setDir, manifest, resumed, opts.restClient);
14883
15035
  const defaultReports = reportAxisDefaults(symbols, Object.keys(defaults).length > 0 ? defaults : void 0);
14884
15036
  const toConfirm = resumed ? [] : defaultReports.filter((r) => (r.rule === "non-interaction" || r.rule === "frequency") && r.domain.length > 1);
14885
15037
  const interactionStatesToConfirm = recordsInteractionState(defaultReports) ? void 0 : interactionDisclosure(manifest.component, defaultReports);
@@ -15069,7 +15221,7 @@ async function runRecordPlan(opts) {
15069
15221
  }
15070
15222
  );
15071
15223
  }
15072
- async function assignRestChannel(setDir, manifest, resumed) {
15224
+ async function assignRestChannel(setDir, manifest, resumed, injected) {
15073
15225
  const current = loadManifest(setDir);
15074
15226
  if (current.channel !== void 0) {
15075
15227
  return { active: true, probeReps: current.probeReps ?? [], note: "REST channel already assigned (frozen with the plan)" };
@@ -15085,7 +15237,7 @@ async function assignRestChannel(setDir, manifest, resumed) {
15085
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"
15086
15238
  };
15087
15239
  }
15088
- const client = await figmaRestClient();
15240
+ const client = injected !== void 0 ? { ok: true, value: injected } : await figmaRestClient();
15089
15241
  if (!client.ok) {
15090
15242
  return client.kind === "no-credential" ? {
15091
15243
  active: false,
@@ -15146,7 +15298,7 @@ async function runRecordRestFetch(opts) {
15146
15298
  remediation: `Record them first: ${tendrilCommand(`record next --set ${opts.setDir}`)} names the calls.`
15147
15299
  });
15148
15300
  }
15149
- const client = await figmaRestClient();
15301
+ const client = opts.restClient !== void 0 ? { ok: true, value: opts.restClient } : await figmaRestClient();
15150
15302
  if (!client.ok) {
15151
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." });
15152
15304
  }
@@ -15649,9 +15801,14 @@ function runRecordStatus(opts) {
15649
15801
  const composition = (() => {
15650
15802
  try {
15651
15803
  const setDir = path45.resolve(opts.setDir);
15652
- const { open, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
15804
+ const { open, proposals, standing, invalid } = compositionPairsFor(setDir, [path45.dirname(setDir)]);
15653
15805
  return {
15654
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 })),
15655
15812
  confirmed: standing.filter((s) => s.status === "confirmed").length,
15656
15813
  declined: standing.filter((s) => s.status === "declined").length,
15657
15814
  invalid
@@ -15685,6 +15842,12 @@ function runRecordStatus(opts) {
15685
15842
  process.stdout.write(`COMPOSITION ${composition.confirmed} confirmed partner pair(s) on this set (composed pins apply at generation)
15686
15843
  `);
15687
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
+ }
15688
15851
  if (!("unavailable" in composition) && composition.invalid > 0) {
15689
15852
  process.stdout.write(
15690
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)
@@ -16250,6 +16413,177 @@ var init_profile_input = __esm({
16250
16413
  }
16251
16414
  });
16252
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
+
16253
16587
  // packages/cli/src/commands/verify.ts
16254
16588
  var verify_exports = {};
16255
16589
  __export(verify_exports, {
@@ -16273,8 +16607,8 @@ __export(verify_exports, {
16273
16607
  runVerify: () => runVerify,
16274
16608
  verdictCaveatsFor: () => verdictCaveatsFor
16275
16609
  });
16276
- import { existsSync as existsSync39, readFileSync as readFileSync35, rmSync as rmSync7, writeFileSync as writeFileSync17 } from "node:fs";
16277
- 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";
16278
16612
  function interactionCoverage(behaviors) {
16279
16613
  const parity = behaviors.filter((b) => b.id.startsWith("parity:"));
16280
16614
  const content = behaviors.filter((b) => b.id.startsWith("content-prop-renders("));
@@ -16512,15 +16846,17 @@ function compositionReport(input) {
16512
16846
  crops: input.crops ?? { unavailable: cropsUnavailable }
16513
16847
  };
16514
16848
  }
16515
- function eyeCheck(bundleDir) {
16516
- return {
16849
+ function eyeCheck(bundleDir, sheet) {
16850
+ const base = {
16517
16851
  command: tendrilCommand(`inspect "${bundleDir}"`),
16518
- sheetPath: path49.join(bundleDir, "verify-evidence", "inspect.html"),
16852
+ sheetPath: path50.join(bundleDir, "verify-evidence", "inspect.html"),
16519
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."
16520
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 } : {} };
16521
16857
  }
16522
16858
  function evidenceArtifacts(evidenceDir, reps) {
16523
- const named = (name) => existsSync39(path49.join(evidenceDir, name)) ? name : null;
16859
+ const named = (name) => existsSync40(path50.join(evidenceDir, name)) ? name : null;
16524
16860
  return {
16525
16861
  legend: named("diff-legend.txt"),
16526
16862
  configs: reps.map((rep) => {
@@ -16568,7 +16904,7 @@ function taskFromManifest(opts, manifest, setDir) {
16568
16904
  const adapterSlugs = Object.keys(manifest.propAdapter);
16569
16905
  const unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
16570
16906
  const adapterOnly = adapterSlugs.filter((s) => !recordedSlugs.includes(s));
16571
- 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));
16572
16908
  const authored = (() => {
16573
16909
  if (registry !== void 0) return void 0;
16574
16910
  try {
@@ -16629,19 +16965,19 @@ function verdictCaveatsFor(input) {
16629
16965
  async function runVerify(opts) {
16630
16966
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
16631
16967
  let recordingSetDrift;
16632
- const setOverride = opts.set !== void 0 ? path49.resolve(callerCwd, opts.set) : void 0;
16633
- opts = { ...opts, bundleDir: path49.resolve(callerCwd, opts.bundleDir), ...setOverride !== void 0 ? { set: setOverride } : {} };
16634
- 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)) {
16635
16971
  fail(opts, ExitCode.InputValidation, {
16636
16972
  error: `bundle directory not found: ${opts.bundleDir}`,
16637
16973
  code: "bundle-missing",
16638
16974
  remediation: "Point at a generated bundle directory (containing component.json, the entry module, and styles.css)."
16639
16975
  });
16640
16976
  }
16641
- const manifestPath2 = path49.join(opts.bundleDir, "component.json");
16977
+ const manifestPath2 = path50.join(opts.bundleDir, "component.json");
16642
16978
  let manifest;
16643
- if (existsSync39(manifestPath2)) {
16644
- const { manifest: parsed, issues } = readBundleManifest(readFileSync35(manifestPath2, "utf8"));
16979
+ if (existsSync40(manifestPath2)) {
16980
+ const { manifest: parsed, issues } = readBundleManifest(readFileSync36(manifestPath2, "utf8"));
16645
16981
  if (issues.length > 0) {
16646
16982
  fail(opts, ExitCode.InputValidation, {
16647
16983
  error: `bundle manifest rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -16672,21 +17008,21 @@ async function runVerify(opts) {
16672
17008
  task = registry;
16673
17009
  } else if (manifest !== void 0) {
16674
17010
  const resolveSetDir = (p) => {
16675
- if (path49.isAbsolute(p)) return p;
16676
- const fromRepo = path49.resolve(REPO_ROOT, p);
16677
- if (existsSync39(fromRepo)) return fromRepo;
16678
- 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);
16679
17015
  };
16680
17016
  const setDir = opts.set ?? resolveSetDir(manifest.provenance.recordingSet.path);
16681
- 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))) {
16682
17018
  fail(opts, ExitCode.RecordingIncomplete, {
16683
17019
  error: `recording set not found or unmanifested: ${setDir}`,
16684
17020
  code: "recording-set-missing",
16685
17021
  remediation: "Pass --set <dir> pointing at the recording set this bundle was generated from."
16686
17022
  });
16687
17023
  }
16688
- const registry = Object.values(TASKS).find((t) => path49.resolve(t.set) === path49.resolve(setDir));
16689
- 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"))) {
16690
17026
  const recordedSlugs = registry.configs.map((c) => c.rep);
16691
17027
  const adapterSlugs = Object.keys(manifest.propAdapter);
16692
17028
  unmapped = recordedSlugs.filter((s) => !adapterSlugs.includes(s));
@@ -16718,9 +17054,9 @@ async function runVerify(opts) {
16718
17054
  recordingSetDrift = { stamped: manifest.provenance.recordingSet.hash, current: hash };
16719
17055
  }
16720
17056
  for (const name of [manifest.entry, "styles.css", "tokens.css"]) {
16721
- const p = path49.join(opts.bundleDir, name);
16722
- if (!existsSync39(p)) continue;
16723
- 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)));
16724
17060
  if (issues.length > 0) {
16725
17061
  fail(opts, ExitCode.InputValidation, {
16726
17062
  error: `bundle source rejected at ingest: ${issues.map((i) => i.message).join("; ")}`,
@@ -16768,7 +17104,7 @@ async function runVerify(opts) {
16768
17104
  });
16769
17105
  }
16770
17106
  const bar = BARS2[opts.bar];
16771
- const evidenceDir = path49.join(opts.bundleDir, "verify-evidence");
17107
+ const evidenceDir = path50.join(opts.bundleDir, "verify-evidence");
16772
17108
  rmSync7(evidenceDir, { recursive: true, force: true });
16773
17109
  const scores = await scoreBundleForTask(task, opts.bundleDir, bar, { evidenceDir, onProgress: emitProgress });
16774
17110
  const quality = await checkBundleQuality(
@@ -16786,7 +17122,7 @@ async function runVerify(opts) {
16786
17122
  // ASKED, never "follows every convention".
16787
17123
  loadCodebaseProfile(opts, opts.profile) ?? void 0
16788
17124
  );
16789
- 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");
16790
17126
  const occlusion = await checkSiblingOcclusion(task, opts.bundleDir, bundleCss, { ...authorityConfigs !== void 0 ? { authority: authorityConfigs } : {} });
16791
17127
  const parity = await checkHoverParity(task, opts.bundleDir, authorityConfigs ?? task.configs, {
16792
17128
  ...opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {},
@@ -16807,10 +17143,10 @@ async function runVerify(opts) {
16807
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.`);
16808
17144
  }
16809
17145
  const verifyCallerCwd = process.env["INIT_CWD"] ?? process.cwd();
16810
- 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: [] };
16811
17147
  const staticComposedChecks = composedChecks(opts.bundleDir, task.entry, verifyPins.pins);
16812
17148
  const renderExpectations = verifyPins.pins.map((pin) => ({
16813
- modulePath: path49.join(composedModuleDir(pin.partnerName), pin.entryComponent),
17149
+ modulePath: path50.join(composedModuleDir(pin.partnerName), pin.entryComponent),
16814
17150
  component: pin.entryComponent,
16815
17151
  stampName: `${pin.pairKey}:${pin.entryComponent}`,
16816
17152
  hostReps: [...new Set(pin.instances.map((i) => i.hostRep))],
@@ -16869,6 +17205,59 @@ async function runVerify(opts) {
16869
17205
  })();
16870
17206
  const compositionBlock = compositionReport({ availability, structural, crops, regions: regionsOut, composedPairs: composedPairs.size });
16871
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
+ })();
16872
17261
  const report = {
16873
17262
  bundle: opts.bundleDir,
16874
17263
  scoredFiles,
@@ -16962,7 +17351,7 @@ async function runVerify(opts) {
16962
17351
  behaviors,
16963
17352
  evidence: { dir: evidenceDir, ...evidenceArtifacts(evidenceDir, statuses.map((s) => s.rep)) },
16964
17353
  composition: compositionBlock,
16965
- verdict: ok ? "verified" : "verification-failed",
17354
+ verdict: verdictWord,
16966
17355
  // Run-23 R5: the one-word verdict printed beside "coverage
16967
17356
  // denominator unknown" and "operability unverified" read, to a
16968
17357
  // JSON consumer who stops at `verdict`, as fully verified — the
@@ -16971,14 +17360,7 @@ async function runVerify(opts) {
16971
17360
  // consumer cannot claim it was never offered. Exit codes are
16972
17361
  // unchanged, deliberately: each claim in the report is true; the
16973
17362
  // caveats say which questions were never answered.
16974
- verdictCaveats: verdictCaveatsFor({
16975
- latticeBlock,
16976
- operability: coverage.operability,
16977
- compositionUnavailable: "unavailable" in compositionBlock,
16978
- fontsSubstituted: substitutedFamilies.length > 0,
16979
- pixelOnlyInteractionPoses: unmappedInteractionEvidence.length,
16980
- hoverBudgetNondefault: opts.hoverTimeoutMs !== void 0 && opts.hoverTimeoutMs !== DEFAULT_HOVER_BUDGET_MS
16981
- }),
17363
+ verdictCaveats,
16982
17364
  // Machine-readable cause (adversarial review): a cert-bar failure
16983
17365
  // with zero pixel/behavior/composition failures was only
16984
17366
  // explainable from stderr prose.
@@ -16991,7 +17373,25 @@ async function runVerify(opts) {
16991
17373
  motion: motionDisclosure(opts.bundleDir, task.set),
16992
17374
  ...recordingSetDrift !== void 0 ? { recordingSetDrift } : {},
16993
17375
  ...substitutedFamilies.length > 0 ? { substitutedFamilies } : {},
16994
- 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 } : {}
16995
17395
  };
16996
17396
  emitData(opts, report, () => {
16997
17397
  for (const s of statuses) {
@@ -17123,7 +17523,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17123
17523
  }
17124
17524
  process.stdout.write(`environment: chrome=${report.environment.chromeVersion ?? report.environment.chrome} fonts=${report.environment.fontsManifestSha256 ?? "UNRESOLVED"}
17125
17525
  `);
17126
- 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")));
17127
17527
  if (shippedFonts.length > 0 && substitutedFamilies.length === 0) {
17128
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)
17129
17529
  `);
@@ -17136,8 +17536,58 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17136
17536
  }
17137
17537
  process.stdout.write(`evidence: ${evidenceDir} (render/ref/diff per config)
17138
17538
  `);
17139
- 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"
17140
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
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
+ }
17141
17591
  });
17142
17592
  if (opts.bar === "cert" && substitutedFamilies.length > 0) {
17143
17593
  const names = substitutedFamilies.map((f) => `"${f}"`).join(", ");
@@ -17178,7 +17628,7 @@ ${certified}/${statuses.length} certified \xB7 ${report.coverage.pass}/${statuse
17178
17628
  persistReport(opts, report, evidenceDir);
17179
17629
  }
17180
17630
  function persistReport(opts, report, evidenceDir) {
17181
- if (!existsSync39(evidenceDir)) return;
17631
+ if (!existsSync40(evidenceDir)) return;
17182
17632
  const rulerExit = process.exitCode === void 0 ? 0 : Number(process.exitCode);
17183
17633
  const withExit = {
17184
17634
  ...report,
@@ -17186,9 +17636,9 @@ function persistReport(opts, report, evidenceDir) {
17186
17636
  ...rulerExit !== 0 ? { rulerRefusal: EXIT_REFUSALS[rulerExit] ?? `ruler exited ${rulerExit}` } : {}
17187
17637
  };
17188
17638
  try {
17189
- writeFileSync17(
17190
- path49.join(evidenceDir, VERIFY_REPORT_FILENAME),
17191
- `${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)}
17192
17642
  `
17193
17643
  );
17194
17644
  } catch (e) {
@@ -17206,6 +17656,8 @@ var init_verify = __esm({
17206
17656
  init_src5();
17207
17657
  init_environment();
17208
17658
  init_font_guidance();
17659
+ init_compose2();
17660
+ init_inspect();
17209
17661
  init_invocation();
17210
17662
  init_output();
17211
17663
  init_profile_input();
@@ -17238,20 +17690,31 @@ __export(engine_exports, {
17238
17690
  runEngineBrief: () => runEngineBrief,
17239
17691
  runEngineScore: () => runEngineScore
17240
17692
  });
17241
- import { appendFileSync, existsSync as existsSync40, mkdirSync as mkdirSync12, readFileSync as readFileSync36, writeFileSync as writeFileSync18 } from "node:fs";
17242
- 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";
17243
17695
  function resolveEngineTask(opts, callerCwd) {
17244
- const asPath = path50.resolve(callerCwd, opts.taskOrSet);
17245
- 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"));
17246
17698
  const registry = TASKS[opts.taskOrSet];
17247
17699
  if (registry !== void 0 && !isSet) return { task: registry, name: opts.taskOrSet, ref: opts.taskOrSet, disclosures: [], interactionEvidence: void 0 };
17248
17700
  if (isSet) {
17701
+ const manifest = loadManifest(asPath);
17702
+ const missing = manifest.reps.filter(
17703
+ (r) => !repEnvelopeExists(asPath, r.slug, "metadata") || !existsSync41(path51.join(asPath, r.slug, "get_design_context.json"))
17704
+ );
17705
+ if (missing.length > 0) {
17706
+ fail(opts, ExitCode.RecordingIncomplete, {
17707
+ error: `recording set ${asPath} is missing envelopes for ${missing.length} rep(s): ${missing.slice(0, 3).map((r) => r.slug).join(", ")}${missing.length > 3 ? ", \u2026" : ""}`,
17708
+ code: "recording-incomplete",
17709
+ remediation: `Resume the recording (\`${tendrilCommand(`record next --set ${asPath}`)}\`) until \`${tendrilCommand(`record status --set ${asPath}`)}\` reports the set complete, then re-run this.`
17710
+ });
17711
+ }
17249
17712
  try {
17250
17713
  const authored = authorTaskFromSet(asPath);
17251
17714
  for (const d of authored.disclosures) warn(opts, d);
17252
17715
  return {
17253
17716
  task: authored.task,
17254
- name: path50.basename(asPath),
17717
+ name: path51.basename(asPath),
17255
17718
  ref: asPath,
17256
17719
  disclosures: authored.disclosures,
17257
17720
  interactionEvidence: authored.api.interactionEvidence,
@@ -17280,9 +17743,9 @@ function runEngineBrief(opts) {
17280
17743
  const { task, name, ref, disclosures } = resolveEngineTask(opts, callerCwd);
17281
17744
  void reportRunPresence(name, "implementing");
17282
17745
  const bar = BARS3[opts.bar];
17283
- if (existsSync40(path50.join(task.set, "recording-set.json"))) {
17746
+ if (existsSync41(path51.join(task.set, "recording-set.json"))) {
17284
17747
  try {
17285
- 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]);
17286
17749
  if (skippedParent !== void 0) {
17287
17750
  disclosures.push(
17288
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.`
@@ -17291,7 +17754,12 @@ function runEngineBrief(opts) {
17291
17754
  if (open.length > 0) {
17292
17755
  const componentNames = [...new Set(open.map((p) => p.displayName))];
17293
17756
  disclosures.push(
17294
- `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."}`
17295
17763
  );
17296
17764
  }
17297
17765
  } catch (err) {
@@ -17308,9 +17776,9 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
17308
17776
  const brief = buildBrief(task.systemApi, bar, { colorScheme: recordingIsDark(task) ? "dark" : "light" }) + disclosureBlock + motionBriefSection(task.set) + conventions;
17309
17777
  const segments = buildSegments(task, "files");
17310
17778
  let notRecorded;
17311
- const manifestPath2 = path50.join(task.set, "recording-set.json");
17312
- if (existsSync40(manifestPath2)) {
17313
- 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;
17314
17782
  }
17315
17783
  const unverified = notRecorded !== void 0 && notRecorded !== "" ? `
17316
17784
 
@@ -17318,7 +17786,7 @@ ${disclosures.map((d) => `- ${d}`).join("\n")}` : "";
17318
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.
17319
17787
  ${notRecorded}` : "";
17320
17788
  let fontProvisioning;
17321
- if (existsSync40(manifestPath2)) {
17789
+ if (existsSync41(manifestPath2)) {
17322
17790
  const missingFams = unprovisionedFamilies(task.set);
17323
17791
  const unprovided = unprovisionedFaces(task.set);
17324
17792
  const weightOnly = missingFams.length === 0;
@@ -17340,7 +17808,7 @@ ${notRecorded}` : "";
17340
17808
  };
17341
17809
  }
17342
17810
  }
17343
- 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]);
17344
17812
  const jsxProp = ([k, v]) => typeof v === "string" ? `${k}=${JSON.stringify(v)}` : `${k}={${JSON.stringify(v)}}`;
17345
17813
  const composedBlock = pinsResult.pins.length === 0 && pinsResult.issues.length === 0 ? "" : (pinsResult.pins.length > 0 ? `
17346
17814
 
@@ -17376,10 +17844,10 @@ ${pinsResult.issues.map((i) => `- ${i}`).join("\n")}` : "");
17376
17844
 
17377
17845
  === TASK PAYLOAD (recorded truth, verbatim) ===
17378
17846
  ${segments}`;
17379
- const payloadFile = path50.resolve(callerCwd, opts.out ?? `tendril-out/${name}-brief.md`);
17380
- const candidateDirSuggestion = path50.resolve(callerCwd, `tendril-out/${name}-candidate`);
17381
- mkdirSync12(path50.dirname(payloadFile), { recursive: true });
17382
- 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);
17383
17851
  emitData(
17384
17852
  opts,
17385
17853
  {
@@ -17425,7 +17893,7 @@ ${segments}`;
17425
17893
  // command must search the same bundle roots the pins came
17426
17894
  // from, or the oracle and the brief describe different worlds.
17427
17895
  `Run \`${tendrilCommand(
17428
- `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`
17429
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.`,
17430
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).",
17431
17899
  "Never edit recording sets; never claim scores yourself \u2014 only the score command's output counts."
@@ -17440,8 +17908,8 @@ ${segments}`;
17440
17908
  );
17441
17909
  }
17442
17910
  function appendScoreHistory(candidateDir, entry) {
17443
- const file = path50.join(candidateDir, "score-history.jsonl");
17444
- 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;
17445
17913
  const round = entry.event === "round-start" ? starts + 1 : Math.max(1, starts);
17446
17914
  appendFileSync(file, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), round, ...entry })}
17447
17915
  `);
@@ -17449,10 +17917,10 @@ function appendScoreHistory(candidateDir, entry) {
17449
17917
  async function runEngineScore(opts) {
17450
17918
  requireEntitlement(opts);
17451
17919
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
17452
- const candidateDir = path50.resolve(callerCwd, opts.candidateDir);
17920
+ const candidateDir = path51.resolve(callerCwd, opts.candidateDir);
17453
17921
  const { task, name, apiPin, interactionEvidence, unmappedInteractionEvidence: interactionEvidenceUnmapped, satisfiabilityDemoted, expertContract } = resolveEngineTask(opts, callerCwd);
17454
17922
  void reportRunPresence(name, "implementing");
17455
- if (!existsSync40(candidateDir)) {
17923
+ if (!existsSync41(candidateDir)) {
17456
17924
  fail(opts, ExitCode.InputValidation, {
17457
17925
  error: `candidate directory not found: ${candidateDir}`,
17458
17926
  code: "candidate-missing",
@@ -17477,10 +17945,10 @@ async function runEngineScore(opts) {
17477
17945
  for (const g of missingWeights(task.set)) {
17478
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)`);
17479
17947
  }
17480
- if (opts.rebind !== true && existsSync40(path50.join(candidateDir, "component.json"))) {
17948
+ if (opts.rebind !== true && existsSync41(path51.join(candidateDir, "component.json"))) {
17481
17949
  const prior = (() => {
17482
17950
  try {
17483
- const read = readBundleManifest(readFileSync36(path50.join(candidateDir, "component.json"), "utf8"));
17951
+ const read = readBundleManifest(readFileSync37(path51.join(candidateDir, "component.json"), "utf8"));
17484
17952
  return read.manifest === void 0 ? { unreadable: true } : { hash: read.manifest.provenance.recordingSet.hash, path: read.manifest.provenance.recordingSet.path };
17485
17953
  } catch {
17486
17954
  return { unreadable: true };
@@ -17502,13 +17970,13 @@ async function runEngineScore(opts) {
17502
17970
  }
17503
17971
  }
17504
17972
  const bar = BARS3[opts.bar];
17505
- const evidenceDir = path50.join(candidateDir, "verify-evidence");
17973
+ const evidenceDir = path51.join(candidateDir, "verify-evidence");
17506
17974
  const scores = await scoreBundleForTask(task, candidateDir, bar, { evidenceDir, onProgress: emitProgress });
17507
17975
  const hoverOpts = opts.hoverTimeoutMs !== void 0 ? { hoverBudgetMs: opts.hoverTimeoutMs } : {};
17508
17976
  const parity = await checkHoverParity(task, candidateDir, task.configs, { ...hoverOpts, failureShotDir: evidenceDir });
17509
17977
  const occlusionRows = await checkSiblingOcclusion(task, candidateDir, candidateCss(candidateDir), { authority: task.configs });
17510
17978
  const occlusionFailures = occlusionRows.filter((o) => !o.pass);
17511
- 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]);
17512
17980
  const composition = composedChecks(candidateDir, task.entry, scorePins.pins);
17513
17981
  const behaviors = [...await checkBehaviors(task, candidateDir, hoverOpts), ...parity, ...composition];
17514
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)";
@@ -17646,7 +18114,18 @@ ${scorePins.issues.map((i) => `- ${i}`).join("\n")}` : "";
17646
18114
  // bundles verify then FAILED on the interaction-evidence gate —
17647
18115
  // the oracle must say what verify will say, including this.
17648
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" } : {},
17649
- 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
+ } : {}
17650
18129
  },
17651
18130
  () => {
17652
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}]` : ""}
@@ -17702,6 +18181,7 @@ var init_engine2 = __esm({
17702
18181
  "packages/cli/src/commands/engine.ts"() {
17703
18182
  "use strict";
17704
18183
  init_src3();
18184
+ init_src();
17705
18185
  init_src7();
17706
18186
  init_src4();
17707
18187
  init_environment();
@@ -17727,11 +18207,11 @@ var codeconnect_exports = {};
17727
18207
  __export(codeconnect_exports, {
17728
18208
  runCodeConnect: () => runCodeConnect
17729
18209
  });
17730
- import { existsSync as existsSync41, readFileSync as readFileSync37, writeFileSync as writeFileSync19 } from "node:fs";
17731
- 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";
17732
18212
  function runCodeConnect(opts) {
17733
18213
  const callerCwd = process.env["INIT_CWD"] ?? process.cwd();
17734
- const bundleDir = path51.resolve(callerCwd, opts.bundleDir);
18214
+ const bundleDir = path52.resolve(callerCwd, opts.bundleDir);
17735
18215
  let url;
17736
18216
  try {
17737
18217
  url = new URL(opts.figmaUrl);
@@ -17747,7 +18227,7 @@ function runCodeConnect(opts) {
17747
18227
  }
17748
18228
  let manifest;
17749
18229
  try {
17750
- const read = readBundleManifest(readFileSync37(path51.join(bundleDir, "component.json"), "utf8"));
18230
+ const read = readBundleManifest(readFileSync38(path52.join(bundleDir, "component.json"), "utf8"));
17751
18231
  if (read.manifest === void 0) throw new Error(read.issues.map((i) => i.message).join("; ") || "no component.json");
17752
18232
  manifest = read.manifest;
17753
18233
  } catch (err) {
@@ -17757,8 +18237,8 @@ function runCodeConnect(opts) {
17757
18237
  remediation: "Point at a bundle produced by the Tendril pipeline (it carries component.json), and verify it first."
17758
18238
  });
17759
18239
  }
17760
- const setDir = path51.resolve(callerCwd, opts.set ?? manifest.provenance.recordingSet.path);
17761
- 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"))) {
17762
18242
  fail(opts, ExitCode.InputValidation, {
17763
18243
  error: `recording set not found at ${setDir}`,
17764
18244
  code: "codeconnect-no-set",
@@ -17780,9 +18260,9 @@ function runCodeConnect(opts) {
17780
18260
  const recManifest = loadManifest(setDir);
17781
18261
  const poseNames = recManifest.latticeNames ?? recManifest.reps.map((r) => {
17782
18262
  const meta = resolveRepEnvelopePath(setDir, r.slug, "metadata");
17783
- if (!existsSync41(meta)) return void 0;
18263
+ if (!existsSync42(meta)) return void 0;
17784
18264
  try {
17785
- return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync37(meta, "utf8"))))?.[1];
18265
+ return /name="([^"]*)"/.exec(envelopeTextContent(JSON.parse(readFileSync38(meta, "utf8"))))?.[1];
17786
18266
  } catch {
17787
18267
  return void 0;
17788
18268
  }
@@ -17847,7 +18327,7 @@ function runCodeConnect(opts) {
17847
18327
  axisLines.push(`const ${varName} = instance.getEnum(${q(axis)}, { ${entries.join(", ")} })`);
17848
18328
  fragmentVars.push(varName);
17849
18329
  }
17850
- const entryRel = path51.relative(callerCwd, path51.join(bundleDir, manifest.entry));
18330
+ const entryRel = path52.relative(callerCwd, path52.join(bundleDir, manifest.entry));
17851
18331
  const trust = manifest.trustStatement.split("\n")[0] ?? "";
17852
18332
  const lines = [
17853
18333
  `// url=${opts.figmaUrl}`,
@@ -17871,8 +18351,8 @@ function runCodeConnect(opts) {
17871
18351
  `}`,
17872
18352
  ``
17873
18353
  ].join("\n");
17874
- const outFile = path51.resolve(callerCwd, opts.out ?? path51.join(bundleDir, `${component}.figma.ts`));
17875
- writeFileSync19(outFile, lines);
18354
+ const outFile = path52.resolve(callerCwd, opts.out ?? path52.join(bundleDir, `${component}.figma.ts`));
18355
+ writeFileSync20(outFile, lines);
17876
18356
  emitData(
17877
18357
  opts,
17878
18358
  {
@@ -17912,17 +18392,17 @@ var init_codeconnect = __esm({
17912
18392
 
17913
18393
  // packages/mcp/src/server.ts
17914
18394
  import { createHash as createHash11 } from "node:crypto";
17915
- 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";
17916
18396
  import os8 from "node:os";
17917
- import path52 from "node:path";
18397
+ import path53 from "node:path";
17918
18398
  import { fileURLToPath as fileURLToPath6 } from "node:url";
17919
18399
  import { z as z15 } from "zod";
17920
18400
  function sourceHash() {
17921
- const dir = path52.dirname(fileURLToPath6(import.meta.url));
18401
+ const dir = path53.dirname(fileURLToPath6(import.meta.url));
17922
18402
  const h = createHash11("sha256");
17923
- 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()) {
17924
18404
  h.update(f);
17925
- h.update(readFileSync38(path52.join(dir, f)));
18405
+ h.update(readFileSync39(path53.join(dir, f)));
17926
18406
  }
17927
18407
  return h.digest("hex").slice(0, 16);
17928
18408
  }
@@ -17930,10 +18410,10 @@ var REPO_ROOT3, CLI_BIN, BUNDLED_CLI, CLI_SPAWN, str, optStr, TOOLS, BOOT_HASH;
17930
18410
  var init_server = __esm({
17931
18411
  "packages/mcp/src/server.ts"() {
17932
18412
  "use strict";
17933
- REPO_ROOT3 = path52.resolve(path52.dirname(fileURLToPath6(import.meta.url)), "..", "..", "..");
17934
- CLI_BIN = path52.join(REPO_ROOT3, "packages", "cli", "src", "bin.ts");
17935
- BUNDLED_CLI = path52.join(path52.dirname(fileURLToPath6(import.meta.url)), "tendril.js");
17936
- 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] };
17937
18417
  str = (d) => z15.string().describe(d);
17938
18418
  optStr = (d) => z15.string().optional().describe(d);
17939
18419
  TOOLS = [
@@ -17964,13 +18444,13 @@ var init_server = __esm({
17964
18444
  const single = i["metadata"];
17965
18445
  const parts = i["metadataParts"];
17966
18446
  if (single !== void 0 || parts !== void 0) {
17967
- 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");
17968
18448
  if (single !== void 0) {
17969
- writeFileSync20(tmp, single);
18449
+ writeFileSync21(tmp, single);
17970
18450
  argvOut.push("--metadata-raw-file", tmp);
17971
18451
  } else {
17972
18452
  if (parts.length === 0) throw new Error("`metadataParts` must be a non-empty array of block texts");
17973
- writeFileSync20(tmp, JSON.stringify(parts));
18453
+ writeFileSync21(tmp, JSON.stringify(parts));
17974
18454
  argvOut.push("--metadata-raw-parts-file", tmp);
17975
18455
  }
17976
18456
  }
@@ -18045,12 +18525,12 @@ var init_server = __esm({
18045
18525
  },
18046
18526
  {
18047
18527
  name: "tendril_publish_wait",
18048
- 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).',
18049
18529
  schema: z15.object({
18050
18530
  bundleDir: str("the same bundle directory tendril_publish was called with"),
18051
18531
  portal: optStr("portal origin override (must match tendril_publish's)")
18052
18532
  }),
18053
- 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"]] : []]
18054
18534
  },
18055
18535
  {
18056
18536
  name: "tendril_record_next",
@@ -18099,14 +18579,14 @@ var init_server = __esm({
18099
18579
  const bridge = (label, single, parts) => {
18100
18580
  if (single !== void 0 && parts !== void 0) throw new Error(`pass at most one of \`${label}\` and \`${label}Parts\``);
18101
18581
  if (single === void 0 && parts === void 0) return;
18102
- 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");
18103
18583
  if (single !== void 0) {
18104
- writeFileSync20(tmp, single);
18584
+ writeFileSync21(tmp, single);
18105
18585
  argvOut.push(`--${label}-file`, tmp);
18106
18586
  } else {
18107
18587
  const blocks = parts;
18108
18588
  if (blocks.length === 0) throw new Error(`\`${label}Parts\` must be a non-empty array of block texts`);
18109
- writeFileSync20(tmp, JSON.stringify(blocks));
18589
+ writeFileSync21(tmp, JSON.stringify(blocks));
18110
18590
  argvOut.push(`--${label}-parts-file`, tmp);
18111
18591
  }
18112
18592
  };
@@ -18147,12 +18627,12 @@ var init_server = __esm({
18147
18627
  const file = i["file"];
18148
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)");
18149
18629
  if (file !== void 0) return [...base, "--file", file];
18150
- 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");
18151
18631
  if (text !== void 0) {
18152
- writeFileSync20(tmp, text);
18632
+ writeFileSync21(tmp, text);
18153
18633
  return [...base, "--file", tmp, "--raw"];
18154
18634
  }
18155
- writeFileSync20(tmp, JSON.stringify(texts));
18635
+ writeFileSync21(tmp, JSON.stringify(texts));
18156
18636
  return [...base, "--file", tmp, "--raw-parts"];
18157
18637
  }
18158
18638
  },
@@ -18212,7 +18692,7 @@ var init_server = __esm({
18212
18692
  },
18213
18693
  {
18214
18694
  name: "tendril_engine_score",
18215
- 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.",
18216
18696
  schema: z15.object({
18217
18697
  taskOrSet: str("reference task name or recording-set directory"),
18218
18698
  candidateDir: str("directory containing the proposed bundle files"),
@@ -18255,7 +18735,7 @@ var init_server = __esm({
18255
18735
  },
18256
18736
  {
18257
18737
  name: "tendril_verify",
18258
- 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.",
18259
18739
  schema: z15.object({
18260
18740
  bundleDir: str("bundle directory to verify"),
18261
18741
  bar: optStr("pass (default) or cert"),
@@ -18309,13 +18789,13 @@ __export(permissions_exports, {
18309
18789
  runPermissions: () => runPermissions,
18310
18790
  writeSelection: () => writeSelection
18311
18791
  });
18312
- 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";
18313
18793
  import os9 from "node:os";
18314
- import path53 from "node:path";
18794
+ import path54 from "node:path";
18315
18795
  function mergeAllowlist(file, entries, denyEntries = []) {
18316
18796
  let settings = {};
18317
- if (existsSync43(file) && readFileSync39(file, "utf8").trim() !== "") {
18318
- settings = JSON.parse(readFileSync39(file, "utf8"));
18797
+ if (existsSync44(file) && readFileSync40(file, "utf8").trim() !== "") {
18798
+ settings = JSON.parse(readFileSync40(file, "utf8"));
18319
18799
  if (settings === null || typeof settings !== "object" || Array.isArray(settings)) throw new Error("settings root is not an object");
18320
18800
  }
18321
18801
  const permissions = settings["permissions"] ??= {};
@@ -18335,8 +18815,8 @@ function mergeAllowlist(file, entries, denyEntries = []) {
18335
18815
  }
18336
18816
  if (added.length > 0 || denyAdded.length > 0) {
18337
18817
  allow.push(...added);
18338
- mkdirSync13(path53.dirname(file), { recursive: true });
18339
- writeFileSync21(file, `${JSON.stringify(settings, null, 2)}
18818
+ mkdirSync13(path54.dirname(file), { recursive: true });
18819
+ writeFileSync22(file, `${JSON.stringify(settings, null, 2)}
18340
18820
  `);
18341
18821
  }
18342
18822
  return { added, alreadyPresent, denyAdded };
@@ -18400,7 +18880,7 @@ async function runPermissions(flags) {
18400
18880
  }
18401
18881
  if (flags.write) {
18402
18882
  const base = process.env["INIT_CWD"] ?? process.cwd();
18403
- 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");
18404
18884
  const { entries, denyEntries } = writeSelection(result, flags.user === true);
18405
18885
  if (flags.dryRun) {
18406
18886
  emitData(flags, { file, wouldAdd: entries, wouldDeny: denyEntries }, () => {
@@ -18543,168 +19023,6 @@ var init_permissions = __esm({
18543
19023
  }
18544
19024
  });
18545
19025
 
18546
- // packages/cli/src/commands/inspect.ts
18547
- var inspect_exports = {};
18548
- __export(inspect_exports, {
18549
- INSPECT_DESCRIPTION: () => INSPECT_DESCRIPTION,
18550
- runInspect: () => runInspect
18551
- });
18552
- import { existsSync as existsSync44, readFileSync as readFileSync40, writeFileSync as writeFileSync22 } from "node:fs";
18553
- import path54 from "node:path";
18554
- function readVerifyReport(evidenceDir) {
18555
- const p = path54.join(evidenceDir, VERIFY_REPORT_FILENAME);
18556
- if (!existsSync44(p)) return void 0;
18557
- try {
18558
- return JSON.parse(readFileSync40(p, "utf8"));
18559
- } catch {
18560
- return void 0;
18561
- }
18562
- }
18563
- function verdictBlock(report) {
18564
- if (report === void 0) {
18565
- 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>`;
18566
- }
18567
- const buckets = /* @__PURE__ */ new Map();
18568
- for (const c of report.configs) buckets.set(c.status, (buckets.get(c.status) ?? 0) + 1);
18569
- const split = [...buckets.entries()].map(([k, n]) => `<span class="tier t-${esc(k)}">${n} ${esc(k)}</span>`).join("");
18570
- const failed = report.behaviors.filter((b) => !b.pass);
18571
- 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>`;
18572
- }
18573
- function scoreLine(report, rep) {
18574
- const c = report?.configs.find((x) => x.rep === rep);
18575
- if (c === void 0) return "";
18576
- const ex = c.exact !== void 0 ? ` <span class="exact">measured ${c.exact.similarity.toFixed(6)} / ${c.exact.inkRecall.toFixed(6)}</span>` : "";
18577
- const why = Array.isArray(c.demotedBy) && c.demotedBy.length > 0 ? `<span class="demoted">demoted: ${esc(c.demotedBy.join("; "))}</span>` : "";
18578
- const err = typeof c.error === "string" ? `<span class="demoted">${esc(c.error)}</span>` : "";
18579
- 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>`;
18580
- }
18581
- async function runInspect(opts) {
18582
- if (opts.describe) {
18583
- printDescription(INSPECT_DESCRIPTION);
18584
- return;
18585
- }
18586
- const bundleDir = path54.resolve(opts.bundleDir);
18587
- const evidenceDir = path54.join(bundleDir, "verify-evidence");
18588
- const manifestPath2 = path54.join(bundleDir, "component.json");
18589
- if (!existsSync44(evidenceDir) || !existsSync44(manifestPath2)) {
18590
- fail(opts, ExitCode.InputValidation, {
18591
- error: `nothing to inspect in ${bundleDir} \u2014 ${existsSync44(manifestPath2) ? "no verify-evidence directory" : "no component.json"}`,
18592
- code: "no-evidence",
18593
- remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
18594
- });
18595
- }
18596
- const { manifest } = readBundleManifest(readFileSync40(manifestPath2, "utf8"));
18597
- if (manifest === void 0) {
18598
- fail(opts, ExitCode.InputValidation, {
18599
- error: "component.json did not parse as a bundle manifest",
18600
- code: "no-mount-contract",
18601
- remediation: "Re-emit the bundle (score/verify rewrite component.json), then re-run inspect."
18602
- });
18603
- }
18604
- const setDir = path54.resolve(opts.set ?? manifest.provenance.recordingSet.path);
18605
- const report = readVerifyReport(evidenceDir);
18606
- const reps = Object.keys(manifest.propAdapter).filter((rep) => existsSync44(path54.join(evidenceDir, `${rep}-ref.png`)) && existsSync44(path54.join(evidenceDir, `${rep}-render.png`)));
18607
- if (reps.length === 0) {
18608
- fail(opts, ExitCode.InputValidation, {
18609
- error: "verify-evidence holds no ref/render pairs for this bundle's configs",
18610
- code: "no-evidence",
18611
- remediation: `Run \`${tendrilCommand(`verify ${bundleDir}`)}\` first \u2014 inspect reads the ref/render evidence that run writes.`
18612
- });
18613
- }
18614
- let crops = 0;
18615
- const sections = [];
18616
- for (const rep of reps) {
18617
- const ref = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-ref.png`)));
18618
- const render = new Uint8Array(readFileSync40(path54.join(evidenceDir, `${rep}-render.png`)));
18619
- const nodes = smallSemanticNodes(setDir, rep, opts.maxArea ?? 1024).slice(0, 12);
18620
- const cells = [];
18621
- for (const [i, n] of nodes.entries()) {
18622
- const rect = { x: n.x, y: n.y, w: n.w, h: n.h };
18623
- try {
18624
- writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-ref.png`), zoomCrop(ref, rect));
18625
- writeFileSync22(path54.join(evidenceDir, `${rep}-inspect-${i}-render.png`), zoomCrop(render, rect));
18626
- } catch {
18627
- continue;
18628
- }
18629
- crops += 1;
18630
- cells.push(
18631
- `<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>`
18632
- );
18633
- }
18634
- sections.push(
18635
- `<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>`
18636
- );
18637
- }
18638
- for (const c of report?.configs ?? []) {
18639
- if (reps.includes(c.rep)) continue;
18640
- 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>`);
18641
- }
18642
- const sheet = path54.join(evidenceDir, "inspect.html");
18643
- writeFileSync22(
18644
- sheet,
18645
- `<!doctype html><meta charset="utf-8"><title>${esc(manifest.name)} \u2014 tendril inspect</title><style>
18646
- body{font:14px/1.45 system-ui,sans-serif;margin:24px;background:#fff;color:#111}
18647
- h1{font-size:20px} h2{font-size:16px;border-top:1px solid #ddd;padding-top:16px}
18648
- .pair,.full{display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
18649
- .full img{max-width:400px;border:1px solid #ccc} .pair img{border:1px solid #ccc;image-rendering:pixelated}
18650
- figure{margin:0 0 16px} figcaption{margin-bottom:4px} em{display:block;color:#666;font-style:normal;font-size:12px}
18651
- .grid{display:flex;flex-wrap:wrap;gap:20px;margin-top:12px} .none{color:#666}
18652
- .verdict{border:1px solid #ddd;border-left-width:5px;padding:12px 16px;margin:16px 0;background:#fafafa}
18653
- .verdict.ok{border-left-color:#1a7f37} .verdict.bad{border-left-color:#b35900} .verdict.none{border-left-color:#999}
18654
- .verdict h2{border:0;padding:0;margin:0 0 8px} .verdict h3{font-size:13px;margin:12px 0 4px;color:#444}
18655
- .verdict small{font-weight:400;color:#666}
18656
- .tiers{margin:0;display:flex;gap:8px;flex-wrap:wrap}
18657
- .tier{display:inline-block;padding:1px 8px;border:1px solid #ccc;border-radius:10px;font-size:12px;background:#fff}
18658
- .t-certified{border-color:#1a7f37;color:#1a7f37} .t-pass{border-color:#8a6d00;color:#8a6d00} .t-fail{border-color:#b3261e;color:#b3261e}
18659
- .caveats,.fails{margin:4px 0 0;padding-left:20px;color:#444} .caveats li,.fails li{margin:2px 0}
18660
- .scores{margin:4px 0 10px;color:#333} .exact{color:#666;font-size:12px} .demoted{color:#b3261e;font-size:12px;margin-left:8px}
18661
- .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}
18662
- </style><h1>${esc(manifest.name)} \u2014 verdict and evidence</h1>
18663
- ${verdictBlock(report)}
18664
- <p>Below, every crop is a recorded node small enough that global metrics weight it as a rounding error.
18665
- Scan the pairs: anything present on the left and missing/invisible on the right is a defect,
18666
- whatever the scores said. Verdicts come from <code>tendril verify</code> \u2014 this sheet only shows.</p>
18667
- ${sections.join("\n")}
18668
- <section><h2>Diff colours</h2><div class="legend">${esc(DIFF_LEGEND_TEXT)}</div></section>
18669
- `
18670
- );
18671
- emitData(opts, { sheet, configs: reps.length, crops }, () => {
18672
- process.stdout.write(`inspect sheet: ${sheet}
18673
- ${reps.length} config(s), ${crops} detail crop pair(s) \u2014 open the sheet and scan recorded vs rendered
18674
- `);
18675
- });
18676
- }
18677
- var INSPECT_DESCRIPTION, esc;
18678
- var init_inspect = __esm({
18679
- "packages/cli/src/commands/inspect.ts"() {
18680
- "use strict";
18681
- init_src3();
18682
- init_src4();
18683
- init_src5();
18684
- init_describe();
18685
- init_invocation();
18686
- init_output();
18687
- INSPECT_DESCRIPTION = {
18688
- name: "inspect",
18689
- summary: "Build an eye-verifiable detail sheet from verify evidence: magnified ref-vs-render crops of every small recorded node (icons, controls, marks).",
18690
- args: [{ name: "bundleDir", required: true, description: "bundle directory (must carry component.json and a verify-evidence dir from a prior `tendril verify`)" }],
18691
- flags: [
18692
- { flag: "--set <dir>", description: "recording set override (default: the bundle's provenance path)" },
18693
- { flag: "--max-area <px2>", description: "node area ceiling for the detail sweep", default: "1024" },
18694
- { flag: "--json", description: "Machine-readable output" }
18695
- ],
18696
- output: {
18697
- sheet: "string \u2014 path to the generated inspect.html",
18698
- configs: "number \u2014 configs with evidence found",
18699
- crops: "number \u2014 detail crop pairs written"
18700
- },
18701
- exitCodes: { 0: "sheet written", 3: "no verify evidence to inspect (run `tendril verify` first)" },
18702
- examples: ["tendril inspect ./src/components/Banner", "tendril inspect ./bundle --set ./tendril/recordings/banner"]
18703
- };
18704
- esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18705
- }
18706
- });
18707
-
18708
19026
  // packages/cli/src/commands/login.ts
18709
19027
  var login_exports = {};
18710
19028
  __export(login_exports, {
@@ -19280,12 +19598,28 @@ var init_share = __esm({
19280
19598
  // packages/cli/src/commands/publish.ts
19281
19599
  var publish_exports = {};
19282
19600
  __export(publish_exports, {
19601
+ approveWaitPhase: () => approveWaitPhase,
19283
19602
  resolveOrigin: () => resolveOrigin2,
19284
- runPublish: () => runPublish
19603
+ runPublish: () => runPublish,
19604
+ spendPendingApproval: () => spendPendingApproval
19285
19605
  });
19286
19606
  import { existsSync as existsSync47, readFileSync as readFileSync43, rmSync as rmSync10, writeFileSync as writeFileSync25 } from "node:fs";
19287
19607
  import path57 from "node:path";
19288
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
+ }
19289
19623
  const bundleDir = path57.resolve(opts.bundleDir);
19290
19624
  const bundle = readBundle(opts, bundleDir);
19291
19625
  const report = bundle.report;
@@ -19399,9 +19733,11 @@ async function runPublish(opts) {
19399
19733
  const origin = resolveOrigin2(opts);
19400
19734
  const client = opts.client ?? httpClient(opts, origin);
19401
19735
  if (opts.approveWait === true) {
19402
- await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
19736
+ const phase = await approveWaitPhase(opts, client, bundleDir, { componentName, figmaFile });
19737
+ if (phase === "yielded") return;
19403
19738
  }
19404
19739
  void reportRunPresence(componentName, "publishing");
19740
+ emitProgress(0, 1, "requesting the upload plan from the portal");
19405
19741
  let opened = await client.begin({
19406
19742
  componentName,
19407
19743
  figmaFile,
@@ -19428,6 +19764,7 @@ async function runPublish(opts) {
19428
19764
  }
19429
19765
  refuse2(opts, opened, "publish-refused");
19430
19766
  }
19767
+ if (opts.approveWait === true) spendPendingApproval();
19431
19768
  const uploaded = [];
19432
19769
  for (const object of opened.value.plan.objects) {
19433
19770
  const file = path57.join(bundleDir, object.relPath);
@@ -19447,6 +19784,7 @@ async function runPublish(opts) {
19447
19784
  uploaded.push({ relPath: object.relPath, deduplicated: sent.value.deduplicated });
19448
19785
  emitProgress(uploaded.length, opened.value.plan.objects.length, `uploading ${object.relPath}`);
19449
19786
  }
19787
+ emitProgress(uploaded.length, uploaded.length, "upload complete \u2014 committing the publication");
19450
19788
  const committed = await client.commit({ publicationId: opened.value.publicationId });
19451
19789
  if (committed.ok) {
19452
19790
  await endRunPresence(componentName);
@@ -19588,7 +19926,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
19588
19926
  const who = await client.whoami?.();
19589
19927
  const asAccount = who?.ok === true && who.value.email !== "" ? ` signed in as ${who.value.email}` : "";
19590
19928
  if (opts.approveStart === true) {
19591
- 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() };
19592
19930
  writeFileSync25(pendingApprovalPath(), `${JSON.stringify(pending, null, 2)}
19593
19931
  `, { mode: 384 });
19594
19932
  await endRunPresence(input.componentName);
@@ -19621,7 +19959,7 @@ async function runApprovalFlow(opts, client, bundleDir, input) {
19621
19959
  const decided = await waitForApproval(opts, client, approval);
19622
19960
  if (decided === "approved") return input.begin();
19623
19961
  await endRunPresence(input.componentName);
19624
- failDecision(opts, decided);
19962
+ failDecision(opts, decided === "window-elapsed" ? "expired" : decided);
19625
19963
  }
19626
19964
  async function approveWaitPhase(opts, client, bundleDir, subject) {
19627
19965
  const file = pendingApprovalPath();
@@ -19651,12 +19989,41 @@ async function approveWaitPhase(opts, client, bundleDir, subject) {
19651
19989
  }
19652
19990
  const done = () => rmSync10(file, { force: true });
19653
19991
  const decided = await waitForApproval(opts, client, pending);
19654
- done();
19655
- 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 });
19656
20022
  }
19657
20023
  async function waitForApproval(opts, client, approval) {
19658
20024
  const interval = Math.max(1, approval.pollSeconds) * 1e3;
19659
- 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);
19660
20027
  for (let tick = 1; tick <= total; tick += 1) {
19661
20028
  const polled = await client.pollApproval({ approvalId: approval.approvalId });
19662
20029
  if (!polled.ok) refuse2(opts, polled, "approval-poll-refused");
@@ -19664,7 +20031,7 @@ async function waitForApproval(opts, client, approval) {
19664
20031
  emitProgress(tick, total, "waiting for the browser approval");
19665
20032
  await new Promise((resolve) => setTimeout(resolve, interval));
19666
20033
  }
19667
- return "expired";
20034
+ return opts.waitWindowSeconds !== void 0 ? "window-elapsed" : "expired";
19668
20035
  }
19669
20036
  function failDecision(opts, decided) {
19670
20037
  if (decided === "denied") {
@@ -21178,7 +21545,7 @@ function buildProgram() {
21178
21545
  ...local["revoke"] !== void 0 ? { revoke: local["revoke"] } : {}
21179
21546
  });
21180
21547
  });
21181
- 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) => {
21182
21549
  const flags = globalFlags(cmd.parent);
21183
21550
  const local = cmd.opts();
21184
21551
  const { runPublish: runPublish2 } = await Promise.resolve().then(() => (init_publish(), publish_exports));
@@ -21188,7 +21555,10 @@ function buildProgram() {
21188
21555
  ...local["to"] !== void 0 ? { to: local["to"] } : {},
21189
21556
  ...local["name"] !== void 0 ? { name: local["name"] } : {},
21190
21557
  ...local["approveStart"] !== void 0 ? { approveStart: local["approveStart"] } : {},
21191
- ...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"]) } : {}
21192
21562
  });
21193
21563
  });
21194
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) => {