tink-harness 1.9.3 → 1.9.5

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.3",
4
+ "version": "1.9.5",
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.5] - 2026-06-10
10
+
11
+ ### Added
12
+
13
+ - Run timeline entries now show a colored outcome badge, a monospace timestamp, and harness chips instead of a comma list.
14
+ - The Activity tab gained a summary strip: total runs, run window, and completed/blocked/failed/recorded counts.
15
+ - Memory cards now show reference counts and referencing-harness chips derived from `uses_memory` graph edges.
16
+
17
+ ### Fixed
18
+
19
+ - The lifecycle generator now reads YAML-frontmatter `outcome:` values from run records, so completed runs are counted as successes instead of unknown.
20
+ - Removed a generic "blocked" word match that flagged runs as blocked when the word merely appeared in prose.
21
+
22
+ ## [1.9.4] - 2026-06-10
23
+
24
+ ### Added
25
+
26
+ - 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.
27
+ - Added an evaluation & maintenance history section to the Harnesses tab, fed by `.tink/maintenance/ledger.jsonl` (new `maintenance_events` field in the lifecycle summary).
28
+
29
+ ### Fixed
30
+
31
+ - The lifecycle generator now strips a UTF-8 BOM before parsing JSONL files, so the first ledger entry is no longer dropped.
32
+
9
33
  ## [1.9.3] - 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.3`
3
+ Current version: `1.9.5`
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.3",
3
+ "version": "1.9.5",
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) {
@@ -83,10 +83,13 @@ function extractRefs(text, refs) {
83
83
  function parseRun(filePath, harnessIds, ruleRefs, memoryRefs) {
84
84
  const text = fs.readFileSync(filePath, 'utf8');
85
85
  const statusMatch = text.match(/^Status:\s*([A-Za-z_-]+)/mi);
86
- const status = statusMatch ? statusMatch[1].toLowerCase() : 'unknown';
86
+ const outcomeMatch = text.match(/^outcome:\s*"?([A-Za-z_-]+)"?/mi);
87
+ const status = statusMatch
88
+ ? statusMatch[1].toLowerCase()
89
+ : (outcomeMatch ? outcomeMatch[1].toLowerCase() : 'unknown');
87
90
  const harnesses = extractSelectedHarnesses(text, harnessIds);
88
91
  const failed = /check_failed|failed check|required check failed|verification failed/i.test(text);
89
- const blocked = status === 'blocked' || /check_blocked|blocked/i.test(text);
92
+ const blocked = status === 'blocked' || /check_blocked/i.test(text);
90
93
  const completed = status === 'completed' || status === 'pass' || /npm test passed|verification passed/i.test(text);
91
94
  return {
92
95
  path: filePath,
@@ -517,6 +520,28 @@ function summarize(root) {
517
520
  item.candidate_score = scoreCandidate(item);
518
521
  }
519
522
  const harnessSummaries = [...summaries.values()].sort((a, b) => a.id.localeCompare(b.id));
523
+
524
+ const ledgerPath = path.join(root, '.tink/maintenance/ledger.jsonl');
525
+ const knownHarnessIds = [...summaries.keys()];
526
+ const maintenanceEvents = parseJsonl(ledgerPath)
527
+ .map((entry) => {
528
+ const refs = [...(Array.isArray(entry.files) ? entry.files : []), ...(Array.isArray(entry.evidence) ? entry.evidence : []), String(entry.op_id || '')];
529
+ const related = knownHarnessIds.filter((id) =>
530
+ refs.some((ref) => String(ref).includes(`${id}.md`) || String(ref).includes(`harness:${id}`) || String(ref).includes(`-${id}-`))
531
+ ).sort();
532
+ return {
533
+ timestamp: entry.timestamp || '',
534
+ op_id: entry.op_id || '',
535
+ type: entry.type || 'unknown',
536
+ files: (Array.isArray(entry.files) ? entry.files : []).slice(0, 8),
537
+ result: entry.result || 'unknown',
538
+ approval: entry.approval || '',
539
+ harnesses: related
540
+ };
541
+ })
542
+ .sort((a, b) => String(b.timestamp).localeCompare(String(a.timestamp)))
543
+ .slice(0, 60);
544
+
520
545
  return {
521
546
  generated_at: new Date().toISOString(),
522
547
  run_window: {
@@ -530,11 +555,13 @@ function summarize(root) {
530
555
  '.tink/memory/*.md',
531
556
  '.tink/runs/*.md',
532
557
  '.tink/maintenance/weave-queue.json',
533
- '.tink/maintenance/friction.jsonl'
558
+ '.tink/maintenance/friction.jsonl',
559
+ '.tink/maintenance/ledger.jsonl'
534
560
  ],
535
561
  harnesses: harnessSummaries,
536
562
  graph: buildGraph(harnessSummaries),
537
- timeline: buildTimeline(runs, root)
563
+ timeline: buildTimeline(runs, root),
564
+ maintenance_events: maintenanceEvents
538
565
  };
539
566
  }
540
567
 
@@ -130,6 +130,22 @@ 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',
146
+ runWindow: 'Run window',
147
+ totalRuns: 'Runs',
148
+ refCount: 'References',
133
149
  groups: [
134
150
  ['keep', 'Healthy harnesses', 'Ready to keep using'],
135
151
  ['weave', 'Weave candidates', 'Worth improving next'],
@@ -174,6 +190,22 @@ COPY.ko = {
174
190
  viewAll: '전체 보기',
175
191
  confidenceShort: '신뢰도',
176
192
  routingHelp: 'cast가 생각 보조 overlay 하네스로 라우팅하는 기준입니다.',
193
+ lastUsed: '마지막 사용',
194
+ successes: '성공',
195
+ failures: '실패',
196
+ contextCost: '컨텍스트 비용',
197
+ coUsedWith: '함께 쓰인 하네스',
198
+ safeNextAction: '다음 안전 행동',
199
+ scoreFactors: '점수 요인',
200
+ viewInGraph: '그래프에서 보기',
201
+ historyEyebrow: '히스토리',
202
+ historyTitle: '평가·생성 히스토리',
203
+ historyHelp: '유지보수 장부에 기록된 승인 변경 이력을 최신순으로 보여줍니다.',
204
+ historyEmpty: '아직 장부 기록이 없습니다.',
205
+ sortNote: '사용량 순 정렬',
206
+ runWindow: '기록 기간',
207
+ totalRuns: 'Run 수',
208
+ refCount: '참조 횟수',
177
209
  navLabel: '탐색',
178
210
  operator: '작업자',
179
211
  online: 'Tink 온라인',
@@ -741,15 +773,26 @@ function dedupeTimelineEvents(events = [], harnessIds = null, limit = 8) {
741
773
  }
742
774
 
743
775
  function renderTimelineItems(items, copy) {
744
- return items.map((event) => `
776
+ return items.map((event) => {
777
+ const outcome = timelineOutcomeClass(event);
778
+ const chips = (event.harnesses || []).slice(0, 6);
779
+ return `
745
780
  <li>
746
- <span class="dot ${escapeHtml(timelineOutcomeClass(event))}"></span>
781
+ <span class="dot ${escapeHtml(outcome)}"></span>
747
782
  <div>
748
- <strong>${escapeHtml(timelineOutcomeLabel(event, copy))} - ${escapeHtml(shortDate(event.date))}</strong>
749
- <p>${escapeHtml(formatHarnessList(event.harnesses) || copy.noHarnessRecorded)}</p>
783
+ <div class="run-row">
784
+ <span class="run-badge ${escapeHtml(outcome)}">${escapeHtml(timelineOutcomeLabel(event, copy))}</span>
785
+ <time>${escapeHtml(shortDate(event.date))}</time>
786
+ </div>
787
+ <div class="run-chips">
788
+ ${chips.length
789
+ ? chips.map((id) => `<span class="co-chip">${escapeHtml(id)}</span>`).join('')
790
+ : `<span class="run-empty">${escapeHtml(copy.noHarnessRecorded)}</span>`}
791
+ </div>
750
792
  </div>
751
793
  </li>
752
- `).join('') || `<li><span class="dot observe"></span><div><strong>${escapeHtml(copy.noRunEvents)}</strong><p>${escapeHtml(copy.runRecordsWillAppear)}</p></div></li>`;
794
+ `;
795
+ }).join('') || `<li><span class="dot observe"></span><div><strong>${escapeHtml(copy.noRunEvents)}</strong><p>${escapeHtml(copy.runRecordsWillAppear)}</p></div></li>`;
753
796
  }
754
797
 
755
798
  function renderTimeline(events = [], copy, harnessIds = null, options = {}) {
@@ -860,26 +903,81 @@ function renderSelectedPanel(harnesses, copy) {
860
903
  function renderHarness(item, copy) {
861
904
  const signals = item.signals || {};
862
905
  const score = Number(item.candidate_score?.total || 0);
906
+ const factors = (item.candidate_score?.factors || []).slice(0, 5);
907
+ const coUsed = (signals.co_used_with || []).slice(0, 5);
908
+ const reason = normalizeReason(item.reason, copy);
863
909
  return `
864
- <article class="harness-card ${recommendationClass(item.recommendation)}" data-harness-id="${escapeAttr(item.id)}" data-recommendation="${escapeAttr(item.recommendation || 'unknown')}" tabindex="0" role="button">
865
- <div>
866
- <p class="eyebrow">${escapeHtml(renderCopyValue(item.recommendation, copy))}</p>
867
- <h3>${escapeHtml(item.id)}</h3>
910
+ <article class="harness-card ${recommendationClass(item.recommendation)}" data-harness-id="${escapeAttr(item.id)}" data-recommendation="${escapeAttr(item.recommendation || 'unknown')}">
911
+ <button class="harness-summary" type="button" aria-expanded="false">
912
+ <div>
913
+ <p class="eyebrow">${escapeHtml(renderCopyValue(item.recommendation, copy))}</p>
914
+ <h3>${escapeHtml(item.id)}</h3>
915
+ </div>
916
+ <div class="harness-mini">
917
+ <span>${escapeHtml(copy.uses)} ${escapeHtml(signals.uses ?? 0)}</span>
918
+ <strong>${escapeHtml(score)}</strong>
919
+ </div>
920
+ <span class="chevron" aria-hidden="true"></span>
921
+ </button>
922
+ <div class="harness-detail">
923
+ <div class="harness-detail-inner">
924
+ ${reason ? `<p class="harness-reason">${escapeHtml(reason)}</p>` : ''}
925
+ <dl>
926
+ <div><dt>${escapeHtml(copy.lifecycleState)}</dt><dd>${escapeHtml(renderCopyValue(item.lifecycle_state, copy))}</dd></div>
927
+ <div><dt>${escapeHtml(copy.lastUsed || 'Last used')}</dt><dd>${escapeHtml(signals.last_used ? shortDate(signals.last_used) : renderCopyValue('', copy))}</dd></div>
928
+ <div><dt>${escapeHtml(copy.successes || 'Successes')}</dt><dd>${escapeHtml(signals.successes ?? 0)}</dd></div>
929
+ <div><dt>${escapeHtml(copy.failures || 'Failures')}</dt><dd>${escapeHtml(signals.failures ?? 0)}</dd></div>
930
+ <div><dt>${escapeHtml(copy.blocked)}</dt><dd>${escapeHtml(signals.blocked ?? 0)}</dd></div>
931
+ <div><dt>${escapeHtml(copy.contextCost || 'Context cost')}</dt><dd>${escapeHtml(renderCopyValue(signals.context_cost, copy))}</dd></div>
932
+ </dl>
933
+ ${coUsed.length ? `
934
+ <p class="detail-label">${escapeHtml(copy.coUsedWith || 'Often used with')}</p>
935
+ <div class="co-used-chips">${coUsed.map((related) => `<span class="co-chip">${escapeHtml(related.id)} ×${escapeHtml(related.count)}</span>`).join('')}</div>
936
+ ` : ''}
937
+ ${factors.length ? `
938
+ <p class="detail-label">${escapeHtml(copy.scoreFactors || 'Score factors')}</p>
939
+ <ul class="factor-list">${factors.map((factor) => `<li><span>${escapeHtml(factor.name)}</span><strong>${escapeHtml(factor.points ?? factor.value ?? '')}</strong></li>`).join('')}</ul>
940
+ ` : ''}
941
+ ${item.safe_next_action ? `
942
+ <p class="detail-label">${escapeHtml(copy.safeNextAction || 'Safe next action')}</p>
943
+ <p class="harness-next">${escapeHtml(item.safe_next_action)}</p>
944
+ ` : ''}
945
+ <p class="detail-label">${escapeHtml(copy.evidenceHandles)} (${escapeHtml(String((item.evidence_handles || []).length))})</p>
946
+ <ul class="evidence-list">${renderEvidence(item.evidence_handles, copy)}</ul>
947
+ <button class="link-button" type="button" data-select-harness="${escapeAttr(item.id)}">${escapeHtml(copy.viewInGraph || 'View in graph')} →</button>
948
+ </div>
868
949
  </div>
869
- <strong>${escapeHtml(score)}</strong>
870
- <dl>
871
- <div><dt>${escapeHtml(copy.lifecycleState)}</dt><dd>${escapeHtml(renderCopyValue(item.lifecycle_state, copy))}</dd></div>
872
- <div><dt>${escapeHtml(copy.uses)}</dt><dd>${escapeHtml(signals.uses ?? 0)}</dd></div>
873
- <div><dt>${escapeHtml(copy.blocked)}</dt><dd>${escapeHtml(signals.blocked ?? 0)}</dd></div>
874
- </dl>
875
- <details>
876
- <summary>${escapeHtml(copy.evidenceHandles)} (${escapeHtml(String((item.evidence_handles || []).length))})</summary>
877
- <ul>${renderEvidence(item.evidence_handles, copy)}</ul>
878
- </details>
879
950
  </article>
880
951
  `;
881
952
  }
882
953
 
954
+ function renderHistorySection(events = [], copy) {
955
+ const items = Array.isArray(events) ? events.slice(0, 30) : [];
956
+ return `
957
+ <section class="history-section">
958
+ <div class="panel-title">
959
+ <p class="eyebrow">${escapeHtml(copy.historyEyebrow || 'HISTORY')}</p>
960
+ <h2>${escapeHtml(copy.historyTitle || 'Evaluation & maintenance history')}</h2>
961
+ <p>${escapeHtml(copy.historyHelp || '')}</p>
962
+ </div>
963
+ ${items.length ? `
964
+ <ol class="history-feed">
965
+ ${items.map((event) => `
966
+ <li>
967
+ <span class="history-type ${escapeAttr(String(event.type || 'unknown').replace(/[^a-z0-9_-]/gi, '-'))}">${escapeHtml(event.type || 'unknown')}</span>
968
+ <div>
969
+ <strong>${escapeHtml(shortDate(event.timestamp))} · ${escapeHtml(event.result || '')}</strong>
970
+ ${event.harnesses?.length ? `<p class="history-harnesses">${event.harnesses.map((id) => escapeHtml(id)).join(', ')}</p>` : ''}
971
+ ${event.files?.length ? `<p class="history-files">${event.files.slice(0, 4).map((file) => `<code>${escapeHtml(normalizePath(file).replace(/^.*[\\/]/, ''))}</code>`).join(' ')}</p>` : ''}
972
+ </div>
973
+ </li>
974
+ `).join('')}
975
+ </ol>
976
+ ` : `<p class="empty-note">${escapeHtml(copy.historyEmpty || 'No ledger history yet.')}</p>`}
977
+ </section>
978
+ `;
979
+ }
980
+
883
981
  function renderGraphOverview(graph = {}, copy) {
884
982
  const stats = graphStats(graph);
885
983
  const nodeCounts = new Map(stats.nodeCounts);
@@ -989,19 +1087,26 @@ function renderHomePage(summary, copy, harnesses, harnessIds) {
989
1087
  function renderMemoryPage(summary, copy) {
990
1088
  const harnesses = getVisibleHarnesses(Array.isArray(summary.harnesses) ? summary.harnesses : []);
991
1089
  const refs = new Map();
1090
+ const ensureRef = (key) => {
1091
+ if (!refs.has(key)) refs.set(key, { users: new Set(), count: 0 });
1092
+ return refs.get(key);
1093
+ };
992
1094
  for (const harness of harnesses) {
993
1095
  for (const ref of harness.signals?.memory_refs || []) {
994
- const key = normalizePath(ref);
995
- if (!refs.has(key)) refs.set(key, new Set());
996
- refs.get(key).add(harness.id);
1096
+ ensureRef(normalizePath(ref)).users.add(harness.id);
997
1097
  }
998
1098
  }
1099
+ for (const edge of getRenderableEdges(summary.graph?.edges || [])) {
1100
+ if (edge.type !== 'uses_memory') continue;
1101
+ const entry = ensureRef(normalizePath(String(edge.target).replace(/^memory:/, '')));
1102
+ entry.users.add(shortLabel(edge.source));
1103
+ entry.count += Number(edge.count || 1);
1104
+ }
999
1105
  for (const node of getRenderableNodes(summary.graph?.nodes || [])) {
1000
1106
  if (node.type !== 'memory') continue;
1001
- const key = normalizePath(shortLabel(node.id));
1002
- if (!refs.has(key)) refs.set(key, new Set());
1107
+ ensureRef(normalizePath(String(node.id).replace(/^memory:/, '')));
1003
1108
  }
1004
- const entries = [...refs.entries()].sort(([a], [b]) => a.localeCompare(b));
1109
+ const entries = [...refs.entries()].sort(([, a], [, b]) => b.count - a.count || b.users.size - a.users.size);
1005
1110
  return `
1006
1111
  <section class="page-head">
1007
1112
  <p class="eyebrow">${escapeHtml(copy.memoryEyebrow || 'MEMORY')}</p>
@@ -1010,15 +1115,21 @@ function renderMemoryPage(summary, copy) {
1010
1115
  </section>
1011
1116
  ${entries.length ? `
1012
1117
  <div class="memory-grid">
1013
- ${entries.map(([file, users]) => `
1118
+ ${entries.map(([file, info]) => `
1014
1119
  <article class="insight-card memory-card">
1015
1120
  <h3><code>${escapeHtml(file)}</code></h3>
1016
1121
  <dl>
1017
1122
  <div>
1018
- <dt>${escapeHtml(copy.referencedBy || 'Referenced by')}</dt>
1019
- <dd>${[...users].length ? [...users].sort().map((id) => escapeHtml(id)).join(', ') : escapeHtml(copy.none || 'None')}</dd>
1123
+ <dt>${escapeHtml(copy.refCount || 'References')}</dt>
1124
+ <dd>${escapeHtml(formatNumber(Math.max(info.count, info.users.size)))}</dd>
1020
1125
  </div>
1021
1126
  </dl>
1127
+ <p class="detail-label">${escapeHtml(copy.referencedBy || 'Referenced by')}</p>
1128
+ <div class="co-used-chips">
1129
+ ${[...info.users].length
1130
+ ? [...info.users].sort().map((id) => `<span class="co-chip">${escapeHtml(id)}</span>`).join('')
1131
+ : `<span class="run-empty">${escapeHtml(copy.none || 'None')}</span>`}
1132
+ </div>
1022
1133
  </article>
1023
1134
  `).join('')}
1024
1135
  </div>
@@ -1028,12 +1139,35 @@ function renderMemoryPage(summary, copy) {
1028
1139
 
1029
1140
  function renderActivityPage(summary, copy, harnessIds) {
1030
1141
  const items = dedupeTimelineEvents(summary.timeline || [], harnessIds, 30);
1142
+ const all = Array.isArray(summary.timeline) ? summary.timeline : [];
1143
+ const counts = { success: 0, blocked: 0, failed: 0, recorded: 0 };
1144
+ for (const event of all) {
1145
+ const key = timelineOutcomeClass(event);
1146
+ counts[key in counts ? key : 'recorded'] += 1;
1147
+ }
1148
+ const window = summary.run_window || {};
1149
+ const windowText = window.from && window.to
1150
+ ? `${shortDate(window.from)} ~ ${shortDate(window.to)}`
1151
+ : renderCopyValue('', copy);
1152
+ const summaryCells = [
1153
+ [copy.totalRuns || 'Runs', formatNumber(window.run_count || all.length)],
1154
+ [copy.runWindow || 'Run window', windowText],
1155
+ [copy.timelineCompleted || 'Completed', formatNumber(counts.success)],
1156
+ [copy.timelineBlocked || 'Blocked', formatNumber(counts.blocked)],
1157
+ [copy.timelineFailed || 'Failed', formatNumber(counts.failed)],
1158
+ [copy.timelineRecorded || 'Recorded', formatNumber(counts.recorded)]
1159
+ ];
1031
1160
  return `
1032
1161
  <section class="page-head">
1033
1162
  <p class="eyebrow">${escapeHtml(copy.activityEyebrow || 'ACTIVITY')}</p>
1034
1163
  <h1>${escapeHtml(copy.activityTitle || 'Run activity')}</h1>
1035
1164
  <p>${escapeHtml(copy.activityHelp || '')}</p>
1036
1165
  </section>
1166
+ <section class="activity-summary">
1167
+ ${summaryCells.map(([label, value]) => `
1168
+ <article><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></article>
1169
+ `).join('')}
1170
+ </section>
1037
1171
  <section class="timeline activity-feed">
1038
1172
  <ol>
1039
1173
  ${renderTimelineItems(items, copy)}
@@ -1267,13 +1401,11 @@ function renderScript(harnesses, copy) {
1267
1401
  selectHarness(button.dataset.selectHarness);
1268
1402
  });
1269
1403
  });
1270
- cards.forEach((card) => {
1271
- card.addEventListener('click', () => selectHarness(card.dataset.harnessId));
1272
- card.addEventListener('keydown', (event) => {
1273
- if (event.key === 'Enter' || event.key === ' ') {
1274
- event.preventDefault();
1275
- selectHarness(card.dataset.harnessId);
1276
- }
1404
+ document.querySelectorAll('.harness-summary').forEach((button) => {
1405
+ button.addEventListener('click', () => {
1406
+ const card = button.closest('.harness-card');
1407
+ const expanded = card.classList.toggle('is-expanded');
1408
+ button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
1277
1409
  });
1278
1410
  });
1279
1411
  document.querySelectorAll('[data-filter-rec]').forEach((button) => {
@@ -1999,24 +2131,26 @@ function renderStyles() {
1999
2131
 
2000
2132
  .harness-grid {
2001
2133
  display: grid;
2002
- grid-template-columns: repeat(2, minmax(0, 1fr));
2003
- gap: var(--space-3);
2134
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2135
+ gap: var(--space-2);
2136
+ align-items: start;
2004
2137
  }
2005
2138
 
2006
2139
  .harness-card {
2007
- cursor: pointer;
2008
- transition: opacity 120ms ease, border-color 120ms ease;
2009
- display: grid;
2010
- gap: var(--space-2);
2140
+ transition: opacity 160ms ease, border-color 160ms ease;
2141
+ padding: 0;
2142
+ overflow: hidden;
2011
2143
  }
2012
2144
 
2013
2145
  .harness-card:hover,
2014
- .harness-card:focus-visible,
2015
- .harness-card.is-selected {
2146
+ .harness-card.is-selected,
2147
+ .harness-card.is-expanded {
2016
2148
  border-color: var(--border-hover);
2017
2149
  outline: none;
2018
2150
  }
2019
2151
 
2152
+ .harness-card.is-expanded { border-color: var(--border-strong); }
2153
+
2020
2154
  .harness-card.is-filtered-out { display: none; }
2021
2155
 
2022
2156
  .harness-card.keep { border-top: 2px solid var(--success); }
@@ -2025,32 +2159,206 @@ function renderStyles() {
2025
2159
  .harness-card.merge_candidate { border-top: 2px solid var(--accent); }
2026
2160
  .harness-card.observe { border-top: 2px solid var(--text-secondary); }
2027
2161
 
2028
- .harness-card > div { display: grid; gap: 4px; }
2162
+ .harness-summary {
2163
+ width: 100%;
2164
+ display: grid;
2165
+ grid-template-columns: 1fr auto 14px;
2166
+ align-items: center;
2167
+ gap: var(--space-2);
2168
+ padding: var(--space-3);
2169
+ border: 0;
2170
+ background: transparent;
2171
+ color: var(--text-primary);
2172
+ font-family: var(--font-ui);
2173
+ text-align: left;
2174
+ cursor: pointer;
2175
+ }
2176
+
2177
+ .harness-summary:hover { background: var(--bg-hover); }
2029
2178
 
2030
- .harness-card h3 {
2179
+ .harness-summary .eyebrow { margin: 0 0 2px; font-size: 10px; }
2180
+
2181
+ .harness-summary h3 {
2031
2182
  margin: 0;
2032
- font-size: 16px;
2183
+ font-size: 14px;
2033
2184
  line-height: 1.25;
2034
2185
  font-weight: 600;
2186
+ overflow-wrap: anywhere;
2035
2187
  }
2036
2188
 
2037
- .harness-card > strong {
2038
- margin: 0;
2039
- font-size: 24px;
2189
+ .harness-mini {
2190
+ display: grid;
2191
+ gap: 2px;
2192
+ justify-items: end;
2193
+ }
2194
+
2195
+ .harness-mini span {
2196
+ color: var(--text-secondary);
2197
+ font-size: 11px;
2198
+ white-space: nowrap;
2199
+ }
2200
+
2201
+ .harness-mini strong {
2202
+ font-size: 18px;
2040
2203
  line-height: 1;
2041
2204
  font-family: var(--font-mono);
2042
2205
  font-weight: 600;
2206
+ }
2207
+
2208
+ .chevron {
2209
+ width: 7px;
2210
+ height: 7px;
2211
+ border-right: 1.5px solid var(--text-secondary);
2212
+ border-bottom: 1.5px solid var(--text-secondary);
2213
+ transform: rotate(45deg);
2214
+ transition: transform 240ms ease;
2215
+ justify-self: center;
2216
+ }
2217
+
2218
+ .harness-card.is-expanded .chevron { transform: rotate(225deg); }
2219
+
2220
+ .harness-detail {
2221
+ display: grid;
2222
+ grid-template-rows: 0fr;
2223
+ transition: grid-template-rows 320ms cubic-bezier(0.2, 0.8, 0.2, 1);
2224
+ }
2225
+
2226
+ .harness-card.is-expanded .harness-detail { grid-template-rows: 1fr; }
2227
+
2228
+ .harness-detail-inner {
2229
+ overflow: hidden;
2230
+ min-height: 0;
2231
+ padding: 0 var(--space-3);
2232
+ display: grid;
2233
+ gap: var(--space-2);
2234
+ opacity: 0;
2235
+ transition: opacity 240ms ease 60ms, padding 320ms ease;
2236
+ }
2237
+
2238
+ .harness-card.is-expanded .harness-detail-inner {
2239
+ opacity: 1;
2240
+ padding: var(--space-1) var(--space-3) var(--space-3);
2241
+ }
2242
+
2243
+ .harness-reason {
2244
+ margin: 0;
2245
+ color: var(--text-secondary);
2246
+ font-size: 12px;
2247
+ line-height: 1.5;
2248
+ }
2249
+
2250
+ .detail-label {
2251
+ margin: var(--space-1) 0 0;
2252
+ color: var(--text-secondary);
2253
+ font-size: 10px;
2254
+ text-transform: uppercase;
2255
+ letter-spacing: 0.06em;
2256
+ }
2257
+
2258
+ .co-used-chips { display: flex; flex-wrap: wrap; gap: 6px; }
2259
+
2260
+ .co-chip {
2261
+ border: 1px solid var(--border-default);
2262
+ background: var(--bg-hover);
2263
+ border-radius: var(--radius-sm);
2264
+ padding: 2px 6px;
2265
+ font-size: 11px;
2266
+ font-family: var(--font-mono);
2267
+ color: var(--text-secondary);
2268
+ }
2269
+
2270
+ .factor-list {
2271
+ margin: 0;
2272
+ padding: 0;
2273
+ list-style: none;
2274
+ display: grid;
2275
+ gap: 4px;
2276
+ }
2277
+
2278
+ .factor-list li {
2279
+ display: flex;
2280
+ justify-content: space-between;
2281
+ gap: var(--space-2);
2282
+ font-size: 12px;
2283
+ color: var(--text-secondary);
2284
+ }
2285
+
2286
+ .factor-list strong { font-family: var(--font-mono); color: var(--text-primary); font-weight: 500; }
2287
+
2288
+ .harness-next {
2289
+ margin: 0;
2043
2290
  color: var(--text-primary);
2044
- justify-self: end;
2291
+ font-size: 12px;
2292
+ line-height: 1.5;
2045
2293
  }
2046
2294
 
2047
- .harness-card p {
2295
+ .evidence-list {
2048
2296
  margin: 0;
2297
+ padding-left: 16px;
2049
2298
  color: var(--text-secondary);
2050
- font-size: 13px;
2051
- line-height: 1.45;
2299
+ font-size: 11px;
2300
+ display: grid;
2301
+ gap: 3px;
2302
+ }
2303
+
2304
+ .harness-card .link-button { margin-top: var(--space-1); justify-self: start; }
2305
+
2306
+ .history-section {
2307
+ margin-top: var(--space-6);
2308
+ border-top: 1px solid var(--border-default);
2309
+ padding-top: var(--space-4);
2310
+ }
2311
+
2312
+ .history-feed {
2313
+ margin: var(--space-3) 0 0;
2314
+ padding: 0;
2315
+ list-style: none;
2316
+ display: grid;
2317
+ gap: var(--space-2);
2318
+ }
2319
+
2320
+ .history-feed li {
2321
+ display: grid;
2322
+ grid-template-columns: 110px 1fr;
2323
+ gap: var(--space-3);
2324
+ align-items: start;
2325
+ border: 1px solid var(--border-default);
2326
+ border-radius: var(--radius-md);
2327
+ background: var(--bg-card);
2328
+ padding: var(--space-2) var(--space-3);
2329
+ }
2330
+
2331
+ .history-type {
2332
+ display: inline-block;
2333
+ font-family: var(--font-mono);
2334
+ font-size: 10px;
2335
+ text-transform: uppercase;
2336
+ letter-spacing: 0.05em;
2337
+ color: var(--text-secondary);
2338
+ border: 1px solid var(--border-default);
2339
+ border-radius: var(--radius-sm);
2340
+ padding: 3px 6px;
2341
+ margin-top: 2px;
2342
+ text-align: center;
2343
+ }
2344
+
2345
+ .history-type.memory { color: var(--accent-text); border-color: var(--accent-dim); }
2346
+ .history-type.weave { color: var(--warning); border-color: var(--warning-dim); }
2347
+ .history-type.frog { color: var(--danger); border-color: var(--danger-dim); }
2348
+ .history-type.harness-create,
2349
+ .history-type.harness-edit { color: var(--success); border-color: var(--success-dim); }
2350
+
2351
+ .history-feed strong { font-size: 12px; font-weight: 500; }
2352
+
2353
+ .history-harnesses {
2354
+ margin: 2px 0 0;
2355
+ color: var(--text-secondary);
2356
+ font-size: 11px;
2357
+ font-family: var(--font-mono);
2052
2358
  }
2053
2359
 
2360
+ .history-files { margin: 4px 0 0; display: flex; flex-wrap: wrap; gap: 4px; }
2361
+
2054
2362
  .harness-card dl,
2055
2363
  .selected dl,
2056
2364
  .graph-overview dl {
@@ -2348,6 +2656,79 @@ function renderStyles() {
2348
2656
  .activity-feed { padding: var(--space-4); }
2349
2657
  .activity-feed ol { gap: var(--space-3); }
2350
2658
 
2659
+ .run-row {
2660
+ display: flex;
2661
+ align-items: center;
2662
+ gap: var(--space-2);
2663
+ }
2664
+
2665
+ .run-row time {
2666
+ color: var(--text-secondary);
2667
+ font-size: 11px;
2668
+ font-family: var(--font-mono);
2669
+ }
2670
+
2671
+ .run-badge {
2672
+ display: inline-block;
2673
+ font-size: 10px;
2674
+ font-weight: 500;
2675
+ text-transform: uppercase;
2676
+ letter-spacing: 0.05em;
2677
+ border: 1px solid var(--border-default);
2678
+ border-radius: var(--radius-sm);
2679
+ padding: 2px 6px;
2680
+ color: var(--text-secondary);
2681
+ }
2682
+
2683
+ .run-badge.success { color: var(--success); border-color: var(--success-dim); background: var(--success-dim); }
2684
+ .run-badge.blocked { color: var(--warning); border-color: var(--warning-dim); background: var(--warning-dim); }
2685
+ .run-badge.failed { color: var(--danger); border-color: var(--danger-dim); background: var(--danger-dim); }
2686
+ .run-badge.recorded { background: var(--bg-hover); }
2687
+
2688
+ .run-chips {
2689
+ display: flex;
2690
+ flex-wrap: wrap;
2691
+ gap: 4px;
2692
+ margin-top: 5px;
2693
+ }
2694
+
2695
+ .run-empty {
2696
+ color: var(--text-muted);
2697
+ font-size: 11px;
2698
+ }
2699
+
2700
+ .activity-summary {
2701
+ display: grid;
2702
+ grid-template-columns: repeat(6, minmax(0, 1fr));
2703
+ gap: var(--space-2);
2704
+ margin-bottom: var(--space-3);
2705
+ }
2706
+
2707
+ .activity-summary article {
2708
+ border: 1px solid var(--border-default);
2709
+ border-radius: var(--radius-lg);
2710
+ background: var(--bg-card);
2711
+ padding: var(--space-3);
2712
+ }
2713
+
2714
+ .activity-summary span {
2715
+ display: block;
2716
+ color: var(--text-secondary);
2717
+ font-size: 11px;
2718
+ letter-spacing: 0.06em;
2719
+ text-transform: uppercase;
2720
+ }
2721
+
2722
+ .activity-summary strong {
2723
+ display: block;
2724
+ margin-top: var(--space-1);
2725
+ font-size: 16px;
2726
+ font-family: var(--font-mono);
2727
+ font-weight: 600;
2728
+ letter-spacing: -0.02em;
2729
+ overflow-wrap: anywhere;
2730
+ }
2731
+
2351
2732
  @media (max-width: 1180px) {
2352
2733
  .app-shell { grid-template-columns: 200px minmax(0, 1fr); }
2353
2734
  .right-rail {
@@ -2359,10 +2740,11 @@ function renderStyles() {
2359
2740
  }
2360
2741
  .hero-sidebar { width: 100%; }
2361
2742
  .project-strip-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2362
- .harness-grid,
2743
+ .harness-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2363
2744
  .stats-grid { grid-template-columns: 1fr; }
2364
2745
  .home-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2365
2746
  .home-columns { grid-template-columns: 1fr; }
2747
+ .activity-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2366
2748
  }
2367
2749
 
2368
2750
  @media (max-width: 760px) {
@@ -2384,6 +2766,8 @@ function renderStyles() {
2384
2766
  .stats-grid { grid-template-columns: 1fr; }
2385
2767
  .home-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2386
2768
  .memory-grid { grid-template-columns: 1fr; }
2769
+ .activity-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2770
+ .harness-grid { grid-template-columns: 1fr; }
2387
2771
  .map-head { display: block; }
2388
2772
  .map-controls { margin-top: var(--space-2); width: max-content; }
2389
2773
  .right-rail { padding: var(--space-4); }
@@ -2439,13 +2823,19 @@ function renderReport(summary) {
2439
2823
  <section class="page-head">
2440
2824
  <p class="eyebrow">${escapeHtml(copy.harnessesEyebrow || 'HARNESSES')}</p>
2441
2825
  <h1>${escapeHtml(copy.harnessCards)}</h1>
2442
- <p>${escapeHtml(copy.harnessesHelp || '')}</p>
2826
+ <p>${escapeHtml(copy.harnessesHelp || '')} · ${escapeHtml(copy.sortNote || 'Sorted by usage')}</p>
2443
2827
  </section>
2444
2828
  <section class="harness-section">
2445
2829
  <div class="harness-grid">
2446
- ${harnesses.map((item) => renderHarness(item, copy)).join('\n')}
2830
+ ${[...harnesses]
2831
+ .sort((a, b) =>
2832
+ Number(b.signals?.uses || 0) - Number(a.signals?.uses || 0) ||
2833
+ Number(b.candidate_score?.total || 0) - Number(a.candidate_score?.total || 0) ||
2834
+ a.id.localeCompare(b.id))
2835
+ .map((item) => renderHarness(item, copy)).join('\n')}
2447
2836
  </div>
2448
2837
  </section>
2838
+ ${renderHistorySection(summary.maintenance_events, copy)}
2449
2839
  </section>
2450
2840
  <section class="page" data-page="memory">
2451
2841
  ${renderMemoryPage(summary, copy)}