tink-harness 1.9.2 → 1.9.4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tink",
3
3
  "description": "A small harness layer for Claude Code and Codex.",
4
- "version": "1.9.2",
4
+ "version": "1.9.4",
5
5
  "author": {
6
6
  "name": "dotori"
7
7
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ All notable changes to Tink are tracked here.
6
6
 
7
7
  No unreleased changes yet.
8
8
 
9
+ ## [1.9.4] - 2026-06-10
10
+
11
+ ### Added
12
+
13
+ - Harness cards now sort by usage and open with a smooth expand animation showing richer details: last used, success/failure counts, context cost, co-used harness chips, score factors, safe next action, and evidence handles.
14
+ - Added an evaluation & maintenance history section to the Harnesses tab, fed by `.tink/maintenance/ledger.jsonl` (new `maintenance_events` field in the lifecycle summary).
15
+
16
+ ### Fixed
17
+
18
+ - The lifecycle generator now strips a UTF-8 BOM before parsing JSONL files, so the first ledger entry is no longer dropped.
19
+
20
+ ## [1.9.3] - 2026-06-10
21
+
22
+ ### Added
23
+
24
+ - Graph nodes now drift gently with per-node organic float motion, staggered entrance animation, and a pulse ring on the selected node.
25
+ - Edges fade in progressively and respond to selection with smooth opacity/width transitions.
26
+ - Added a node-type color legend under the graph and polished the tooltip with fade/slide motion.
27
+
28
+ ### Changed
29
+
30
+ - Graph hover now scales the node smoothly instead of only changing the stroke.
31
+ - The graph tab shows only graph-related information; honors prefers-reduced-motion.
32
+
9
33
  ## [1.9.2] - 2026-06-10
10
34
 
11
35
  ### Added
package/VERSIONING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versioning
2
2
 
3
- Current version: `1.9.2`
3
+ Current version: `1.9.4`
4
4
 
5
5
  Tink follows semver from `1.0.0` onward.
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tink-harness",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "Self-growing harnesses for Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,7 @@ function readJson(filePath, fallback) {
15
15
 
16
16
  function readLines(filePath) {
17
17
  if (!fs.existsSync(filePath)) return [];
18
- return fs.readFileSync(filePath, 'utf8').split(/\r?\n/).filter(Boolean);
18
+ return fs.readFileSync(filePath, 'utf8').replace(/^/, '').split(/\r?\n/).filter(Boolean);
19
19
  }
20
20
 
21
21
  function listFiles(dirPath, suffix) {
@@ -517,6 +517,28 @@ function summarize(root) {
517
517
  item.candidate_score = scoreCandidate(item);
518
518
  }
519
519
  const harnessSummaries = [...summaries.values()].sort((a, b) => a.id.localeCompare(b.id));
520
+
521
+ const ledgerPath = path.join(root, '.tink/maintenance/ledger.jsonl');
522
+ const knownHarnessIds = [...summaries.keys()];
523
+ const maintenanceEvents = parseJsonl(ledgerPath)
524
+ .map((entry) => {
525
+ const refs = [...(Array.isArray(entry.files) ? entry.files : []), ...(Array.isArray(entry.evidence) ? entry.evidence : []), String(entry.op_id || '')];
526
+ const related = knownHarnessIds.filter((id) =>
527
+ refs.some((ref) => String(ref).includes(`${id}.md`) || String(ref).includes(`harness:${id}`) || String(ref).includes(`-${id}-`))
528
+ ).sort();
529
+ return {
530
+ timestamp: entry.timestamp || '',
531
+ op_id: entry.op_id || '',
532
+ type: entry.type || 'unknown',
533
+ files: (Array.isArray(entry.files) ? entry.files : []).slice(0, 8),
534
+ result: entry.result || 'unknown',
535
+ approval: entry.approval || '',
536
+ harnesses: related
537
+ };
538
+ })
539
+ .sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)))
540
+ .slice(0, 60);
541
+
520
542
  return {
521
543
  generated_at: new Date().toISOString(),
522
544
  run_window: {
@@ -530,11 +552,13 @@ function summarize(root) {
530
552
  '.tink/memory/*.md',
531
553
  '.tink/runs/*.md',
532
554
  '.tink/maintenance/weave-queue.json',
533
- '.tink/maintenance/friction.jsonl'
555
+ '.tink/maintenance/friction.jsonl',
556
+ '.tink/maintenance/ledger.jsonl'
534
557
  ],
535
558
  harnesses: harnessSummaries,
536
559
  graph: buildGraph(harnessSummaries),
537
- timeline: buildTimeline(runs, root)
560
+ timeline: buildTimeline(runs, root),
561
+ maintenance_events: maintenanceEvents
538
562
  };
539
563
  }
540
564
 
@@ -130,6 +130,19 @@ const COPY = {
130
130
  viewAll: 'View all',
131
131
  confidenceShort: 'Confidence',
132
132
  routingHelp: 'When cast routes a task to a visible-thinking overlay harness.',
133
+ lastUsed: 'Last used',
134
+ successes: 'Successes',
135
+ failures: 'Failures',
136
+ contextCost: 'Context cost',
137
+ coUsedWith: 'Often used with',
138
+ safeNextAction: 'Safe next action',
139
+ scoreFactors: 'Score factors',
140
+ viewInGraph: 'View in graph',
141
+ historyEyebrow: 'HISTORY',
142
+ historyTitle: 'Evaluation & maintenance history',
143
+ historyHelp: 'Approved reusable-state changes from the maintenance ledger, newest first.',
144
+ historyEmpty: 'No ledger history yet.',
145
+ sortNote: 'Sorted by usage',
133
146
  groups: [
134
147
  ['keep', 'Healthy harnesses', 'Ready to keep using'],
135
148
  ['weave', 'Weave candidates', 'Worth improving next'],
@@ -174,6 +187,19 @@ COPY.ko = {
174
187
  viewAll: '전체 보기',
175
188
  confidenceShort: '신뢰도',
176
189
  routingHelp: 'cast가 생각 보조 overlay 하네스로 라우팅하는 기준입니다.',
190
+ lastUsed: '마지막 사용',
191
+ successes: '성공',
192
+ failures: '실패',
193
+ contextCost: '컨텍스트 비용',
194
+ coUsedWith: '함께 쓰인 하네스',
195
+ safeNextAction: '다음 안전 행동',
196
+ scoreFactors: '점수 요인',
197
+ viewInGraph: '그래프에서 보기',
198
+ historyEyebrow: '히스토리',
199
+ historyTitle: '평가·생성 히스토리',
200
+ historyHelp: '유지보수 장부에 기록된 승인 변경 이력을 최신순으로 보여줍니다.',
201
+ historyEmpty: '아직 장부 기록이 없습니다.',
202
+ sortNote: '사용량 순 정렬',
177
203
  navLabel: '탐색',
178
204
  operator: '작업자',
179
205
  online: 'Tink 온라인',
@@ -614,9 +640,10 @@ function renderGraphCanvas(summary, copy) {
614
640
  <svg class="graph-canvas" viewBox="0 0 1090 680" role="img" aria-label="Harness health graph">
615
641
  <rect width="1090" height="680" fill="var(--bg-card)"/>
616
642
  <g class="edges">
617
- ${edges.map((edge) => `
643
+ ${edges.map((edge, index) => `
618
644
  <line
619
645
  class="graph-edge"
646
+ style="--edge-delay: ${Math.min(index * 5, 850)}ms"
620
647
  data-source="${escapeAttr(edge.source)}"
621
648
  data-target="${escapeAttr(edge.target)}"
622
649
  x1="${edge.sourceNode.x.toFixed(1)}"
@@ -630,9 +657,17 @@ function renderGraphCanvas(summary, copy) {
630
657
  `).join('')}
631
658
  </g>
632
659
  <g class="nodes">
633
- ${nodes.map((node) => `
660
+ ${nodes.map((node, index) => {
661
+ const seed = hashString(node.id);
662
+ const floatDuration = (5 + (seed % 50) / 10).toFixed(1);
663
+ const floatDelay = -(seed % 4000);
664
+ const floatX = ((seed % 7) - 3).toFixed(1);
665
+ const floatY = (((seed >> 3) % 7) - 3).toFixed(1);
666
+ return `
667
+ <g class="node-float" style="--float-dur: ${floatDuration}s; --float-delay: ${floatDelay}ms; --float-x: ${floatX}px; --float-y: ${floatY}px">
634
668
  <circle
635
669
  class="graph-node ${node.type === 'harness' ? 'is-interactive' : ''}"
670
+ style="--enter-delay: ${Math.min(index * 9, 1100)}ms"
636
671
  tabindex="${node.type === 'harness' ? '0' : '-1'}"
637
672
  role="${node.type === 'harness' ? 'button' : 'presentation'}"
638
673
  aria-label="${escapeAttr(`${copy.tooltipPrefix}: ${node.label}`)}"
@@ -653,7 +688,9 @@ function renderGraphCanvas(summary, copy) {
653
688
  >
654
689
  <title>${escapeHtml(node.id)}</title>
655
690
  </circle>
656
- `).join('')}
691
+ </g>
692
+ `;
693
+ }).join('')}
657
694
  </g>
658
695
  <g class="labels">
659
696
  ${strongest.map((node) => `
@@ -665,9 +702,13 @@ function renderGraphCanvas(summary, copy) {
665
702
  <div class="map-caption">
666
703
  <span id="graph-status">${escapeHtml(copy.showingAll)}</span>
667
704
  <span>${escapeHtml(copy.nodeSize)}</span>
668
- <span>${escapeHtml(copy.colorType)}</span>
669
705
  <span>${escapeHtml(copy.linesRelations)}</span>
670
706
  </div>
707
+ <div class="map-legend" aria-label="${escapeAttr(copy.nodeTypes || 'Node types')}">
708
+ ${['harness', 'rule', 'memory', 'stage', 'signal', 'evidence', 'score'].map((type) => `
709
+ <span class="legend-chip"><i style="background: ${escapeAttr(TYPE_COLORS[type] || TYPE_COLORS.unknown)}"></i>${escapeHtml(type)}</span>
710
+ `).join('')}
711
+ </div>
671
712
  </section>
672
713
  `;
673
714
  }
@@ -845,26 +886,81 @@ function renderSelectedPanel(harnesses, copy) {
845
886
  function renderHarness(item, copy) {
846
887
  const signals = item.signals || {};
847
888
  const score = Number(item.candidate_score?.total || 0);
889
+ const factors = (item.candidate_score?.factors || []).slice(0, 5);
890
+ const coUsed = (signals.co_used_with || []).slice(0, 5);
891
+ const reason = normalizeReason(item.reason, copy);
848
892
  return `
849
- <article class="harness-card ${recommendationClass(item.recommendation)}" data-harness-id="${escapeAttr(item.id)}" data-recommendation="${escapeAttr(item.recommendation || 'unknown')}" tabindex="0" role="button">
850
- <div>
851
- <p class="eyebrow">${escapeHtml(renderCopyValue(item.recommendation, copy))}</p>
852
- <h3>${escapeHtml(item.id)}</h3>
893
+ <article class="harness-card ${recommendationClass(item.recommendation)}" data-harness-id="${escapeAttr(item.id)}" data-recommendation="${escapeAttr(item.recommendation || 'unknown')}">
894
+ <button class="harness-summary" type="button" aria-expanded="false">
895
+ <div>
896
+ <p class="eyebrow">${escapeHtml(renderCopyValue(item.recommendation, copy))}</p>
897
+ <h3>${escapeHtml(item.id)}</h3>
898
+ </div>
899
+ <div class="harness-mini">
900
+ <span>${escapeHtml(copy.uses)} ${escapeHtml(signals.uses ?? 0)}</span>
901
+ <strong>${escapeHtml(score)}</strong>
902
+ </div>
903
+ <span class="chevron" aria-hidden="true"></span>
904
+ </button>
905
+ <div class="harness-detail">
906
+ <div class="harness-detail-inner">
907
+ ${reason ? `<p class="harness-reason">${escapeHtml(reason)}</p>` : ''}
908
+ <dl>
909
+ <div><dt>${escapeHtml(copy.lifecycleState)}</dt><dd>${escapeHtml(renderCopyValue(item.lifecycle_state, copy))}</dd></div>
910
+ <div><dt>${escapeHtml(copy.lastUsed || 'Last used')}</dt><dd>${escapeHtml(signals.last_used ? shortDate(signals.last_used) : renderCopyValue('', copy))}</dd></div>
911
+ <div><dt>${escapeHtml(copy.successes || 'Successes')}</dt><dd>${escapeHtml(signals.successes ?? 0)}</dd></div>
912
+ <div><dt>${escapeHtml(copy.failures || 'Failures')}</dt><dd>${escapeHtml(signals.failures ?? 0)}</dd></div>
913
+ <div><dt>${escapeHtml(copy.blocked)}</dt><dd>${escapeHtml(signals.blocked ?? 0)}</dd></div>
914
+ <div><dt>${escapeHtml(copy.contextCost || 'Context cost')}</dt><dd>${escapeHtml(renderCopyValue(signals.context_cost, copy))}</dd></div>
915
+ </dl>
916
+ ${coUsed.length ? `
917
+ <p class="detail-label">${escapeHtml(copy.coUsedWith || 'Often used with')}</p>
918
+ <div class="co-used-chips">${coUsed.map((related) => `<span class="co-chip">${escapeHtml(related.id)} ×${escapeHtml(related.count)}</span>`).join('')}</div>
919
+ ` : ''}
920
+ ${factors.length ? `
921
+ <p class="detail-label">${escapeHtml(copy.scoreFactors || 'Score factors')}</p>
922
+ <ul class="factor-list">${factors.map((factor) => `<li><span>${escapeHtml(factor.name)}</span><strong>${escapeHtml(factor.points ?? factor.value ?? '')}</strong></li>`).join('')}</ul>
923
+ ` : ''}
924
+ ${item.safe_next_action ? `
925
+ <p class="detail-label">${escapeHtml(copy.safeNextAction || 'Safe next action')}</p>
926
+ <p class="harness-next">${escapeHtml(item.safe_next_action)}</p>
927
+ ` : ''}
928
+ <p class="detail-label">${escapeHtml(copy.evidenceHandles)} (${escapeHtml(String((item.evidence_handles || []).length))})</p>
929
+ <ul class="evidence-list">${renderEvidence(item.evidence_handles, copy)}</ul>
930
+ <button class="link-button" type="button" data-select-harness="${escapeAttr(item.id)}">${escapeHtml(copy.viewInGraph || 'View in graph')} →</button>
931
+ </div>
853
932
  </div>
854
- <strong>${escapeHtml(score)}</strong>
855
- <dl>
856
- <div><dt>${escapeHtml(copy.lifecycleState)}</dt><dd>${escapeHtml(renderCopyValue(item.lifecycle_state, copy))}</dd></div>
857
- <div><dt>${escapeHtml(copy.uses)}</dt><dd>${escapeHtml(signals.uses ?? 0)}</dd></div>
858
- <div><dt>${escapeHtml(copy.blocked)}</dt><dd>${escapeHtml(signals.blocked ?? 0)}</dd></div>
859
- </dl>
860
- <details>
861
- <summary>${escapeHtml(copy.evidenceHandles)} (${escapeHtml(String((item.evidence_handles || []).length))})</summary>
862
- <ul>${renderEvidence(item.evidence_handles, copy)}</ul>
863
- </details>
864
933
  </article>
865
934
  `;
866
935
  }
867
936
 
937
+ function renderHistorySection(events = [], copy) {
938
+ const items = Array.isArray(events) ? events.slice(0, 30) : [];
939
+ return `
940
+ <section class="history-section">
941
+ <div class="panel-title">
942
+ <p class="eyebrow">${escapeHtml(copy.historyEyebrow || 'HISTORY')}</p>
943
+ <h2>${escapeHtml(copy.historyTitle || 'Evaluation & maintenance history')}</h2>
944
+ <p>${escapeHtml(copy.historyHelp || '')}</p>
945
+ </div>
946
+ ${items.length ? `
947
+ <ol class="history-feed">
948
+ ${items.map((event) => `
949
+ <li>
950
+ <span class="history-type ${escapeAttr(String(event.type || 'unknown').replace(/[^a-z0-9_-]/gi, '-'))}">${escapeHtml(event.type || 'unknown')}</span>
951
+ <div>
952
+ <strong>${escapeHtml(shortDate(event.timestamp))} · ${escapeHtml(event.result || '')}</strong>
953
+ ${event.harnesses?.length ? `<p class="history-harnesses">${event.harnesses.map((id) => escapeHtml(id)).join(', ')}</p>` : ''}
954
+ ${event.files?.length ? `<p class="history-files">${event.files.slice(0, 4).map((file) => `<code>${escapeHtml(normalizePath(file).replace(/^.*[\\/]/, ''))}</code>`).join(' ')}</p>` : ''}
955
+ </div>
956
+ </li>
957
+ `).join('')}
958
+ </ol>
959
+ ` : `<p class="empty-note">${escapeHtml(copy.historyEmpty || 'No ledger history yet.')}</p>`}
960
+ </section>
961
+ `;
962
+ }
963
+
868
964
  function renderGraphOverview(graph = {}, copy) {
869
965
  const stats = graphStats(graph);
870
966
  const nodeCounts = new Map(stats.nodeCounts);
@@ -1252,13 +1348,11 @@ function renderScript(harnesses, copy) {
1252
1348
  selectHarness(button.dataset.selectHarness);
1253
1349
  });
1254
1350
  });
1255
- cards.forEach((card) => {
1256
- card.addEventListener('click', () => selectHarness(card.dataset.harnessId));
1257
- card.addEventListener('keydown', (event) => {
1258
- if (event.key === 'Enter' || event.key === ' ') {
1259
- event.preventDefault();
1260
- selectHarness(card.dataset.harnessId);
1261
- }
1351
+ document.querySelectorAll('.harness-summary').forEach((button) => {
1352
+ button.addEventListener('click', () => {
1353
+ const card = button.closest('.harness-card');
1354
+ const expanded = card.classList.toggle('is-expanded');
1355
+ button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1262
1356
  });
1263
1357
  });
1264
1358
  document.querySelectorAll('[data-filter-rec]').forEach((button) => {
@@ -1764,23 +1858,55 @@ function renderStyles() {
1764
1858
 
1765
1859
  .graph-canvas text {
1766
1860
  pointer-events: none;
1861
+ fill: var(--text-secondary);
1862
+ font-size: 11px;
1863
+ font-family: var(--font-mono);
1864
+ animation: label-in 600ms ease 900ms backwards;
1865
+ }
1866
+
1867
+ @keyframes label-in {
1868
+ from { opacity: 0; }
1869
+ to { opacity: 1; }
1767
1870
  }
1768
1871
 
1769
1872
  .graph-edge {
1770
1873
  stroke: var(--chart-line);
1771
1874
  stroke-width: var(--chart-line-w);
1772
1875
  opacity: 0.24;
1876
+ transition: opacity 220ms ease, stroke-width 220ms ease;
1877
+ animation: edge-in 700ms ease var(--edge-delay, 0ms) backwards;
1878
+ }
1879
+
1880
+ @keyframes edge-in {
1881
+ from { opacity: 0; }
1773
1882
  }
1774
1883
 
1775
1884
  .graph-edge.is-related { opacity: 0.9; stroke-width: 2px; }
1776
1885
 
1886
+ .node-float {
1887
+ animation: node-float var(--float-dur, 6s) ease-in-out var(--float-delay, 0ms) infinite alternate;
1888
+ }
1889
+
1890
+ @keyframes node-float {
1891
+ from { transform: translate(0, 0); }
1892
+ to { transform: translate(var(--float-x, 2px), var(--float-y, -2px)); }
1893
+ }
1894
+
1777
1895
  .graph-node {
1778
1896
  cursor: default;
1779
1897
  outline: none;
1780
- transition: none;
1781
1898
  vector-effect: non-scaling-stroke;
1782
1899
  shape-rendering: geometricPrecision;
1783
1900
  paint-order: stroke fill;
1901
+ transform-box: fill-box;
1902
+ transform-origin: center;
1903
+ transition: opacity 220ms ease, transform 220ms cubic-bezier(0.2, 0.8, 0.3, 1.1), stroke 160ms ease, stroke-opacity 160ms ease, fill-opacity 160ms ease;
1904
+ animation: node-enter 500ms cubic-bezier(0.2, 0.8, 0.3, 1.2) var(--enter-delay, 0ms) backwards;
1905
+ }
1906
+
1907
+ @keyframes node-enter {
1908
+ from { opacity: 0; transform: scale(0.2); }
1909
+ to { opacity: 1; transform: scale(1); }
1784
1910
  }
1785
1911
 
1786
1912
  .graph-node.is-interactive {
@@ -1793,33 +1919,87 @@ function renderStyles() {
1793
1919
  }
1794
1920
 
1795
1921
  .graph-node.is-interactive:hover,
1796
- .graph-node.is-interactive:focus-visible,
1922
+ .graph-node.is-interactive:focus-visible {
1923
+ stroke: var(--accent);
1924
+ stroke-opacity: 1;
1925
+ transform: scale(1.28);
1926
+ }
1927
+
1797
1928
  .graph-node.is-selected {
1798
1929
  stroke: var(--accent);
1799
1930
  stroke-opacity: 1;
1931
+ animation: node-pulse 1.6s ease-in-out infinite;
1932
+ }
1933
+
1934
+ @keyframes node-pulse {
1935
+ 0%, 100% { stroke-width: 1.8px; transform: scale(1.12); }
1936
+ 50% { stroke-width: 5px; transform: scale(1.2); }
1800
1937
  }
1801
1938
 
1802
1939
  .graph-node.is-related { opacity: 1; }
1803
1940
  .graph-node.is-hidden,
1804
1941
  .graph-edge.is-hidden,
1805
1942
  .graph-node.is-filtered-out,
1806
- .graph-edge.is-filtered-out { opacity: 0.15; pointer-events: none; }
1943
+ .graph-edge.is-filtered-out { opacity: 0.12; pointer-events: none; }
1807
1944
 
1808
1945
  .graph-tooltip {
1809
1946
  position: fixed;
1810
1947
  z-index: 20;
1811
- display: none;
1948
+ visibility: hidden;
1949
+ opacity: 0;
1950
+ transform: translateY(4px);
1951
+ transition: opacity 160ms ease, transform 160ms ease, visibility 0s linear 160ms;
1812
1952
  max-width: 230px;
1813
1953
  padding: 8px 10px;
1814
- border: 1px solid var(--border-default);
1954
+ border: 1px solid var(--border-hover);
1815
1955
  border-radius: var(--radius-md);
1816
- background: var(--bg-card);
1956
+ background: var(--bg-selected);
1817
1957
  color: var(--text-primary);
1818
1958
  font-size: 11px;
1819
1959
  line-height: 1.3;
1820
1960
  pointer-events: none;
1961
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
1962
+ }
1963
+ .graph-tooltip.is-visible {
1964
+ visibility: visible;
1965
+ opacity: 1;
1966
+ transform: translateY(0);
1967
+ transition: opacity 160ms ease, transform 160ms ease;
1968
+ }
1969
+
1970
+ .map-legend {
1971
+ display: flex;
1972
+ gap: var(--space-3);
1973
+ margin-top: var(--space-3);
1974
+ padding-top: var(--space-3);
1975
+ border-top: 1px solid var(--border-default);
1976
+ flex-wrap: wrap;
1977
+ }
1978
+
1979
+ .legend-chip {
1980
+ display: inline-flex;
1981
+ align-items: center;
1982
+ gap: 6px;
1983
+ color: var(--text-secondary);
1984
+ font-size: 11px;
1985
+ font-family: var(--font-mono);
1986
+ }
1987
+
1988
+ .legend-chip i {
1989
+ width: 9px;
1990
+ height: 9px;
1991
+ border-radius: 50%;
1992
+ display: inline-block;
1993
+ }
1994
+
1995
+ @media (prefers-reduced-motion: reduce) {
1996
+ .node-float,
1997
+ .graph-node,
1998
+ .graph-edge,
1999
+ .graph-canvas text,
2000
+ .page.is-active { animation: none; }
2001
+ .graph-node { transition: none; }
1821
2002
  }
1822
- .graph-tooltip.is-visible { display: block; }
1823
2003
 
1824
2004
  .map-caption {
1825
2005
  display: flex;
@@ -1898,24 +2078,26 @@ function renderStyles() {
1898
2078
 
1899
2079
  .harness-grid {
1900
2080
  display: grid;
1901
- grid-template-columns: repeat(2, minmax(0, 1fr));
1902
- gap: var(--space-3);
2081
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2082
+ gap: var(--space-2);
2083
+ align-items: start;
1903
2084
  }
1904
2085
 
1905
2086
  .harness-card {
1906
- cursor: pointer;
1907
- transition: opacity 120ms ease, border-color 120ms ease;
1908
- display: grid;
1909
- gap: var(--space-2);
2087
+ transition: opacity 160ms ease, border-color 160ms ease;
2088
+ padding: 0;
2089
+ overflow: hidden;
1910
2090
  }
1911
2091
 
1912
2092
  .harness-card:hover,
1913
- .harness-card:focus-visible,
1914
- .harness-card.is-selected {
2093
+ .harness-card.is-selected,
2094
+ .harness-card.is-expanded {
1915
2095
  border-color: var(--border-hover);
1916
2096
  outline: none;
1917
2097
  }
1918
2098
 
2099
+ .harness-card.is-expanded { border-color: var(--border-strong); }
2100
+
1919
2101
  .harness-card.is-filtered-out { display: none; }
1920
2102
 
1921
2103
  .harness-card.keep { border-top: 2px solid var(--success); }
@@ -1924,32 +2106,206 @@ function renderStyles() {
1924
2106
  .harness-card.merge_candidate { border-top: 2px solid var(--accent); }
1925
2107
  .harness-card.observe { border-top: 2px solid var(--text-secondary); }
1926
2108
 
1927
- .harness-card > div { display: grid; gap: 4px; }
2109
+ .harness-summary {
2110
+ width: 100%;
2111
+ display: grid;
2112
+ grid-template-columns: 1fr auto 14px;
2113
+ align-items: center;
2114
+ gap: var(--space-2);
2115
+ padding: var(--space-3);
2116
+ border: 0;
2117
+ background: transparent;
2118
+ color: var(--text-primary);
2119
+ font-family: var(--font-ui);
2120
+ text-align: left;
2121
+ cursor: pointer;
2122
+ }
2123
+
2124
+ .harness-summary:hover { background: var(--bg-hover); }
1928
2125
 
1929
- .harness-card h3 {
2126
+ .harness-summary .eyebrow { margin: 0 0 2px; font-size: 10px; }
2127
+
2128
+ .harness-summary h3 {
1930
2129
  margin: 0;
1931
- font-size: 16px;
2130
+ font-size: 14px;
1932
2131
  line-height: 1.25;
1933
2132
  font-weight: 600;
2133
+ overflow-wrap: anywhere;
1934
2134
  }
1935
2135
 
1936
- .harness-card > strong {
1937
- margin: 0;
1938
- font-size: 24px;
2136
+ .harness-mini {
2137
+ display: grid;
2138
+ gap: 2px;
2139
+ justify-items: end;
2140
+ }
2141
+
2142
+ .harness-mini span {
2143
+ color: var(--text-secondary);
2144
+ font-size: 11px;
2145
+ white-space: nowrap;
2146
+ }
2147
+
2148
+ .harness-mini strong {
2149
+ font-size: 18px;
1939
2150
  line-height: 1;
1940
2151
  font-family: var(--font-mono);
1941
2152
  font-weight: 600;
2153
+ }
2154
+
2155
+ .chevron {
2156
+ width: 7px;
2157
+ height: 7px;
2158
+ border-right: 1.5px solid var(--text-secondary);
2159
+ border-bottom: 1.5px solid var(--text-secondary);
2160
+ transform: rotate(45deg);
2161
+ transition: transform 240ms ease;
2162
+ justify-self: center;
2163
+ }
2164
+
2165
+ .harness-card.is-expanded .chevron { transform: rotate(225deg); }
2166
+
2167
+ .harness-detail {
2168
+ display: grid;
2169
+ grid-template-rows: 0fr;
2170
+ transition: grid-template-rows 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
2171
+ }
2172
+
2173
+ .harness-card.is-expanded .harness-detail { grid-template-rows: 1fr; }
2174
+
2175
+ .harness-detail-inner {
2176
+ overflow: hidden;
2177
+ min-height: 0;
2178
+ padding: 0 var(--space-3);
2179
+ display: grid;
2180
+ gap: var(--space-2);
2181
+ opacity: 0;
2182
+ transition: opacity 240ms ease 60ms, padding 320ms ease;
2183
+ }
2184
+
2185
+ .harness-card.is-expanded .harness-detail-inner {
2186
+ opacity: 1;
2187
+ padding: var(--space-1) var(--space-3) var(--space-3);
2188
+ }
2189
+
2190
+ .harness-reason {
2191
+ margin: 0;
2192
+ color: var(--text-secondary);
2193
+ font-size: 12px;
2194
+ line-height: 1.5;
2195
+ }
2196
+
2197
+ .detail-label {
2198
+ margin: var(--space-1) 0 0;
2199
+ color: var(--text-secondary);
2200
+ font-size: 10px;
2201
+ text-transform: uppercase;
2202
+ letter-spacing: 0.06em;
2203
+ }
2204
+
2205
+ .co-used-chips { display: flex; flex-wrap: wrap; gap: 6px; }
2206
+
2207
+ .co-chip {
2208
+ border: 1px solid var(--border-default);
2209
+ background: var(--bg-hover);
2210
+ border-radius: var(--radius-sm);
2211
+ padding: 2px 6px;
2212
+ font-size: 11px;
2213
+ font-family: var(--font-mono);
2214
+ color: var(--text-secondary);
2215
+ }
2216
+
2217
+ .factor-list {
2218
+ margin: 0;
2219
+ padding: 0;
2220
+ list-style: none;
2221
+ display: grid;
2222
+ gap: 4px;
2223
+ }
2224
+
2225
+ .factor-list li {
2226
+ display: flex;
2227
+ justify-content: space-between;
2228
+ gap: var(--space-2);
2229
+ font-size: 12px;
2230
+ color: var(--text-secondary);
2231
+ }
2232
+
2233
+ .factor-list strong { font-family: var(--font-mono); color: var(--text-primary); font-weight: 500; }
2234
+
2235
+ .harness-next {
2236
+ margin: 0;
1942
2237
  color: var(--text-primary);
1943
- justify-self: end;
2238
+ font-size: 12px;
2239
+ line-height: 1.5;
1944
2240
  }
1945
2241
 
1946
- .harness-card p {
2242
+ .evidence-list {
1947
2243
  margin: 0;
2244
+ padding-left: 16px;
1948
2245
  color: var(--text-secondary);
1949
- font-size: 13px;
1950
- line-height: 1.45;
2246
+ font-size: 11px;
2247
+ display: grid;
2248
+ gap: 3px;
1951
2249
  }
1952
2250
 
2251
+ .harness-card .link-button { margin-top: var(--space-1); justify-self: start; }
2252
+
2253
+ .history-section {
2254
+ margin-top: var(--space-6);
2255
+ border-top: 1px solid var(--border-default);
2256
+ padding-top: var(--space-4);
2257
+ }
2258
+
2259
+ .history-feed {
2260
+ margin: var(--space-3) 0 0;
2261
+ padding: 0;
2262
+ list-style: none;
2263
+ display: grid;
2264
+ gap: var(--space-2);
2265
+ }
2266
+
2267
+ .history-feed li {
2268
+ display: grid;
2269
+ grid-template-columns: 110px 1fr;
2270
+ gap: var(--space-3);
2271
+ align-items: start;
2272
+ border: 1px solid var(--border-default);
2273
+ border-radius: var(--radius-md);
2274
+ background: var(--bg-card);
2275
+ padding: var(--space-2) var(--space-3);
2276
+ }
2277
+
2278
+ .history-type {
2279
+ display: inline-block;
2280
+ font-family: var(--font-mono);
2281
+ font-size: 10px;
2282
+ text-transform: uppercase;
2283
+ letter-spacing: 0.05em;
2284
+ color: var(--text-secondary);
2285
+ border: 1px solid var(--border-default);
2286
+ border-radius: var(--radius-sm);
2287
+ padding: 3px 6px;
2288
+ margin-top: 2px;
2289
+ text-align: center;
2290
+ }
2291
+
2292
+ .history-type.memory { color: var(--accent-text); border-color: var(--accent-dim); }
2293
+ .history-type.weave { color: var(--warning); border-color: var(--warning-dim); }
2294
+ .history-type.frog { color: var(--danger); border-color: var(--danger-dim); }
2295
+ .history-type.harness-create,
2296
+ .history-type.harness-edit { color: var(--success); border-color: var(--success-dim); }
2297
+
2298
+ .history-feed strong { font-size: 12px; font-weight: 500; }
2299
+
2300
+ .history-harnesses {
2301
+ margin: 2px 0 0;
2302
+ color: var(--text-secondary);
2303
+ font-size: 11px;
2304
+ font-family: var(--font-mono);
2305
+ }
2306
+
2307
+ .history-files { margin: 4px 0 0; display: flex; flex-wrap: wrap; gap: 4px; }
2308
+
1953
2309
  .harness-card dl,
1954
2310
  .selected dl,
1955
2311
  .graph-overview dl {
@@ -2258,7 +2614,7 @@ function renderStyles() {
2258
2614
  }
2259
2615
  .hero-sidebar { width: 100%; }
2260
2616
  .project-strip-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2261
- .harness-grid,
2617
+ .harness-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2262
2618
  .stats-grid { grid-template-columns: 1fr; }
2263
2619
  .home-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2264
2620
  .home-columns { grid-template-columns: 1fr; }
@@ -2338,13 +2694,19 @@ function renderReport(summary) {
2338
2694
  <section class="page-head">
2339
2695
  <p class="eyebrow">${escapeHtml(copy.harnessesEyebrow || 'HARNESSES')}</p>
2340
2696
  <h1>${escapeHtml(copy.harnessCards)}</h1>
2341
- <p>${escapeHtml(copy.harnessesHelp || '')}</p>
2697
+ <p>${escapeHtml(copy.harnessesHelp || '')} · ${escapeHtml(copy.sortNote || 'Sorted by usage')}</p>
2342
2698
  </section>
2343
2699
  <section class="harness-section">
2344
2700
  <div class="harness-grid">
2345
- ${harnesses.map((item) => renderHarness(item, copy)).join('\n')}
2701
+ ${[...harnesses]
2702
+ .sort((a, b) =>
2703
+ Number(b.signals?.uses || 0) - Number(a.signals?.uses || 0) ||
2704
+ Number(b.candidate_score?.total || 0) - Number(a.candidate_score?.total || 0) ||
2705
+ a.id.localeCompare(b.id))
2706
+ .map((item) => renderHarness(item, copy)).join('\n')}
2346
2707
  </div>
2347
2708
  </section>
2709
+ ${renderHistorySection(summary.maintenance_events, copy)}
2348
2710
  </section>
2349
2711
  <section class="page" data-page="memory">
2350
2712
  ${renderMemoryPage(summary, copy)}