tink-harness 1.9.4 → 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.4",
4
+ "version": "1.9.5",
5
5
  "author": {
6
6
  "name": "dotori"
7
7
  }
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ 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
+
9
22
  ## [1.9.4] - 2026-06-10
10
23
 
11
24
  ### Added
package/VERSIONING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Versioning
2
2
 
3
- Current version: `1.9.4`
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.4",
3
+ "version": "1.9.5",
4
4
  "description": "Self-growing harnesses for Claude Code and Codex.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,
@@ -143,6 +143,9 @@ const COPY = {
143
143
  historyHelp: 'Approved reusable-state changes from the maintenance ledger, newest first.',
144
144
  historyEmpty: 'No ledger history yet.',
145
145
  sortNote: 'Sorted by usage',
146
+ runWindow: 'Run window',
147
+ totalRuns: 'Runs',
148
+ refCount: 'References',
146
149
  groups: [
147
150
  ['keep', 'Healthy harnesses', 'Ready to keep using'],
148
151
  ['weave', 'Weave candidates', 'Worth improving next'],
@@ -200,6 +203,9 @@ COPY.ko = {
200
203
  historyHelp: '유지보수 장부에 기록된 승인 변경 이력을 최신순으로 보여줍니다.',
201
204
  historyEmpty: '아직 장부 기록이 없습니다.',
202
205
  sortNote: '사용량 순 정렬',
206
+ runWindow: '기록 기간',
207
+ totalRuns: 'Run 수',
208
+ refCount: '참조 횟수',
203
209
  navLabel: '탐색',
204
210
  operator: '작업자',
205
211
  online: 'Tink 온라인',
@@ -767,15 +773,26 @@ function dedupeTimelineEvents(events = [], harnessIds = null, limit = 8) {
767
773
  }
768
774
 
769
775
  function renderTimelineItems(items, copy) {
770
- return items.map((event) => `
776
+ return items.map((event) => {
777
+ const outcome = timelineOutcomeClass(event);
778
+ const chips = (event.harnesses || []).slice(0, 6);
779
+ return `
771
780
  <li>
772
- <span class="dot ${escapeHtml(timelineOutcomeClass(event))}"></span>
781
+ <span class="dot ${escapeHtml(outcome)}"></span>
773
782
  <div>
774
- <strong>${escapeHtml(timelineOutcomeLabel(event, copy))} - ${escapeHtml(shortDate(event.date))}</strong>
775
- <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>
776
792
  </div>
777
793
  </li>
778
- `).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>`;
779
796
  }
780
797
 
781
798
  function renderTimeline(events = [], copy, harnessIds = null, options = {}) {
@@ -1070,19 +1087,26 @@ function renderHomePage(summary, copy, harnesses, harnessIds) {
1070
1087
  function renderMemoryPage(summary, copy) {
1071
1088
  const harnesses = getVisibleHarnesses(Array.isArray(summary.harnesses) ? summary.harnesses : []);
1072
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
+ };
1073
1094
  for (const harness of harnesses) {
1074
1095
  for (const ref of harness.signals?.memory_refs || []) {
1075
- const key = normalizePath(ref);
1076
- if (!refs.has(key)) refs.set(key, new Set());
1077
- refs.get(key).add(harness.id);
1096
+ ensureRef(normalizePath(ref)).users.add(harness.id);
1078
1097
  }
1079
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
+ }
1080
1105
  for (const node of getRenderableNodes(summary.graph?.nodes || [])) {
1081
1106
  if (node.type !== 'memory') continue;
1082
- const key = normalizePath(shortLabel(node.id));
1083
- if (!refs.has(key)) refs.set(key, new Set());
1107
+ ensureRef(normalizePath(String(node.id).replace(/^memory:/, '')));
1084
1108
  }
1085
- 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);
1086
1110
  return `
1087
1111
  <section class="page-head">
1088
1112
  <p class="eyebrow">${escapeHtml(copy.memoryEyebrow || 'MEMORY')}</p>
@@ -1091,15 +1115,21 @@ function renderMemoryPage(summary, copy) {
1091
1115
  </section>
1092
1116
  ${entries.length ? `
1093
1117
  <div class="memory-grid">
1094
- ${entries.map(([file, users]) => `
1118
+ ${entries.map(([file, info]) => `
1095
1119
  <article class="insight-card memory-card">
1096
1120
  <h3><code>${escapeHtml(file)}</code></h3>
1097
1121
  <dl>
1098
1122
  <div>
1099
- <dt>${escapeHtml(copy.referencedBy || 'Referenced by')}</dt>
1100
- <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>
1101
1125
  </div>
1102
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>
1103
1133
  </article>
1104
1134
  `).join('')}
1105
1135
  </div>
@@ -1109,12 +1139,35 @@ function renderMemoryPage(summary, copy) {
1109
1139
 
1110
1140
  function renderActivityPage(summary, copy, harnessIds) {
1111
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
+ ];
1112
1160
  return `
1113
1161
  <section class="page-head">
1114
1162
  <p class="eyebrow">${escapeHtml(copy.activityEyebrow || 'ACTIVITY')}</p>
1115
1163
  <h1>${escapeHtml(copy.activityTitle || 'Run activity')}</h1>
1116
1164
  <p>${escapeHtml(copy.activityHelp || '')}</p>
1117
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>
1118
1171
  <section class="timeline activity-feed">
1119
1172
  <ol>
1120
1173
  ${renderTimelineItems(items, copy)}
@@ -2603,6 +2656,79 @@ function renderStyles() {
2603
2656
  .activity-feed { padding: var(--space-4); }
2604
2657
  .activity-feed ol { gap: var(--space-3); }
2605
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
+
2606
2732
  @media (max-width: 1180px) {
2607
2733
  .app-shell { grid-template-columns: 200px minmax(0, 1fr); }
2608
2734
  .right-rail {
@@ -2618,6 +2744,7 @@ function renderStyles() {
2618
2744
  .stats-grid { grid-template-columns: 1fr; }
2619
2745
  .home-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2620
2746
  .home-columns { grid-template-columns: 1fr; }
2747
+ .activity-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
2621
2748
  }
2622
2749
 
2623
2750
  @media (max-width: 760px) {
@@ -2639,6 +2766,8 @@ function renderStyles() {
2639
2766
  .stats-grid { grid-template-columns: 1fr; }
2640
2767
  .home-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2641
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; }
2642
2771
  .map-head { display: block; }
2643
2772
  .map-controls { margin-top: var(--space-2); width: max-content; }
2644
2773
  .right-rail { padding: var(--space-4); }