humanish 0.20.3 → 0.20.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.
@@ -417,6 +417,20 @@ a { color: inherit; text-decoration: none; }
417
417
  /* ============================================================ STREAM SURFACES */
418
418
  .surface-fill { position: absolute; inset: 0; width: 100%; height: 100%; }
419
419
  .surface-screenshot { object-fit: contain; object-position: top center; }
420
+ /* Replay scrubber (#292 first slice): a per-turn keyframe timeline built from persisted frames + trace. */
421
+ .replay { position: absolute; inset: 0; display: flex; flex-direction: column; background: #0b0d10; }
422
+ .replay-stage { position: relative; flex: 1 1 auto; min-height: 0; }
423
+ .replay-badge { position: absolute; top: 6px; right: 8px; z-index: 2; font-size: 9px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-2); background: rgba(0,0,0,.55); border: 1px solid var(--line-2); border-radius: 999px; padding: 2px 7px; }
424
+ .replay-bar { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-top: 1px solid var(--line-2); background: var(--surface-2); }
425
+ .replay-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 7px; border: 1px solid var(--line-2); background: transparent; color: var(--text-1); cursor: pointer; }
426
+ .replay-btn:hover { background: var(--surface-3); }
427
+ .replay-range { flex: 1 1 auto; accent-color: var(--accent-color); cursor: pointer; height: 4px; min-width: 40px; }
428
+ .replay-count { flex: 0 0 auto; font-size: 11px; color: var(--text-2); min-width: 44px; text-align: right; }
429
+ .replay-info { flex: 0 0 auto; max-height: 32%; overflow-y: auto; padding: 8px 10px; border-top: 1px solid var(--line-2); background: var(--surface-1); font-size: 12px; line-height: 1.5; color: var(--text-1); }
430
+ .replay-acts { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
431
+ .replay-act { font-family: var(--mono); font-size: 10px; color: var(--text-2); background: var(--surface-3); border: 1px solid var(--line-2); border-radius: 5px; padding: 2px 6px; }
432
+ .replay-narr { white-space: pre-wrap; color: var(--text-1); }
433
+ .replay-dim { color: var(--text-2); font-style: italic; }
420
434
  .live-stream-mount { position: absolute; inset: 0; overflow: hidden; background: #000; }
421
435
  .live-stream-overlay { position: absolute; overflow: hidden; pointer-events: none; z-index: 2; }
422
436
  .live-stream-overlay[data-focus="true"] .bw-lab-dock { max-height: 86px; }
@@ -1143,6 +1157,12 @@ export function observerClientJs() {
1143
1157
  motion: readPref("motion", "full")
1144
1158
  };
1145
1159
  var artifactCache = {};
1160
+ // Replay-scrubber state: per-stream current frame index + auto-advance timer. Kept OUTSIDE the S
1161
+ // render-state so a data-poll re-render preserves the scrub position; indices survive because
1162
+ // render() reads scrubIndexFor() (which defaults to the last frame — also fixing the "finished lane
1163
+ // cuts to black" symptom). Timers are cleared on every render() so playback never orphans.
1164
+ var scrubState = {};
1165
+ var scrubTimers = {};
1146
1166
  var liveStreamFrames = {};
1147
1167
  var liveStreamHost = null;
1148
1168
  var liveStreamLayoutRaf = null;
@@ -1198,7 +1218,9 @@ export function observerClientJs() {
1198
1218
  lock: '<rect x="4.5" y="10" width="15" height="10" rx="2"' + P + '/><path d="M8 10V7a4 4 0 0 1 8 0v3"' + P + '/>',
1199
1219
  caret: '<path d="m6 9 6 6 6-6"' + P + '/>',
1200
1220
  filter: '<path d="M3 5h18l-7 8.2V20l-4-2.2v-4.6L3 5Z"' + P + '/>',
1201
- panelRight: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M15 4v16"' + P + '/>'
1221
+ panelRight: '<rect x="3" y="4" width="18" height="16" rx="2"' + P + '/><path d="M15 4v16"' + P + '/>',
1222
+ play: '<path d="M7 5l12 7-12 7z"' + P + '/>',
1223
+ pause: '<path d="M9 5v14M15 5v14"' + P + '/>'
1202
1224
  };
1203
1225
  function icon(name, size) {
1204
1226
  var s = size || 18;
@@ -1605,14 +1627,147 @@ export function observerClientJs() {
1605
1627
  }
1606
1628
  return '<div class="wait"><div class="wait-inner">' + inner + '</div></div>';
1607
1629
  }
1630
+ // ---------------------------------------------------------------- replay scrubber
1631
+ // A durable per-turn keyframe replay built PURELY from the artifacts already in the bundle: the
1632
+ // ordered actor.items (reasoning/message/ui_action) sequenced against each persisted screenshot.
1633
+ // No new capture, no schema change, no new privacy surface — it reuses the frames' existing
1634
+ // gitignored / publishable:false posture. (#292 first slice.)
1635
+ function replayFrames(s) {
1636
+ var actor = s && s.actor;
1637
+ var items = actor && actor.items;
1638
+ if (!items || !items.length) return [];
1639
+ var frames = [];
1640
+ var narration = [];
1641
+ var actions = [];
1642
+ for (var i = 0; i < items.length; i += 1) {
1643
+ var it = items[i];
1644
+ if (!it) continue;
1645
+ if (it.kind === "reasoning" || it.kind === "message") { if (it.text) narration.push(String(it.text)); }
1646
+ else if (it.kind === "ui_action") { if (it.title) actions.push(String(it.title)); }
1647
+ else if (it.kind === "screenshot" && it.screenshotRef && it.screenshotRef.path) {
1648
+ frames.push({
1649
+ src: it.screenshotRef.path,
1650
+ label: it.title || ("frame " + (frames.length + 1)),
1651
+ redaction: (it.screenshotRef.redaction && it.screenshotRef.redaction !== "none") ? it.screenshotRef.redaction : "raw",
1652
+ narration: narration,
1653
+ actions: actions
1654
+ });
1655
+ narration = [];
1656
+ actions = [];
1657
+ }
1658
+ }
1659
+ return frames;
1660
+ }
1661
+ function streamById(id) {
1662
+ var ss = currentData.streams || [];
1663
+ for (var i = 0; i < ss.length; i += 1) { if (ss[i].id === id) return ss[i]; }
1664
+ return null;
1665
+ }
1666
+ function scrubIndexOf(id, frames) {
1667
+ var v = scrubState[id];
1668
+ if (v == null) return frames.length ? frames.length - 1 : 0; // default: the last real frame
1669
+ v = Number(v);
1670
+ if (!(v >= 0)) v = 0;
1671
+ if (v > frames.length - 1) v = frames.length - 1;
1672
+ return v;
1673
+ }
1674
+ function frameInfoHtml(f) {
1675
+ if (!f) return "";
1676
+ var out = "";
1677
+ var acts = (f.actions || []).filter(Boolean);
1678
+ var narr = (f.narration || []).filter(Boolean);
1679
+ if (acts.length) out += '<div class="replay-acts">' + acts.map(function (a) { return '<span class="replay-act">' + esc(a) + '</span>'; }).join("") + '</div>';
1680
+ if (narr.length) out += '<div class="replay-narr">' + esc(narr.join(NL + NL)) + '</div>';
1681
+ if (!out) out = '<div class="replay-narr replay-dim">No narration recorded before this frame.</div>';
1682
+ return out;
1683
+ }
1684
+ function replayScrubber(s) {
1685
+ var frames = replayFrames(s);
1686
+ var id = s.id;
1687
+ var idx = scrubIndexOf(id, frames);
1688
+ var f = frames[idx];
1689
+ var playing = !!scrubTimers[id];
1690
+ return '<div class="replay" data-stream="' + esc(id) + '">'
1691
+ + '<div class="replay-stage"><img class="surface-fill surface-screenshot" id="replay-img-' + esc(id) + '" src="' + esc(runArtifactHref(f && f.src)) + '" alt="replay frame ' + (idx + 1) + '"/>'
1692
+ + '<span class="replay-badge" id="replay-badge-' + esc(id) + '" title="frame capture fidelity">' + esc(f ? f.redaction : "raw") + '</span></div>'
1693
+ + '<div class="replay-bar">'
1694
+ + '<button class="replay-btn" id="replay-play-' + esc(id) + '" data-action="scrub:' + esc(id) + ':play" aria-label="Play replay">' + icon(playing ? "pause" : "play", 13) + '</button>'
1695
+ + '<button class="replay-btn" data-action="scrub:' + esc(id) + ':-1" aria-label="Previous frame">' + icon("chevL", 13) + '</button>'
1696
+ + '<input class="replay-range" type="range" data-role="scrub" data-stream="' + esc(id) + '" min="0" max="' + (frames.length - 1) + '" step="1" value="' + idx + '" aria-label="Scrub frames"/>'
1697
+ + '<button class="replay-btn" data-action="scrub:' + esc(id) + ':1" aria-label="Next frame">' + icon("chevR", 13) + '</button>'
1698
+ + '<span class="replay-count mono" id="replay-count-' + esc(id) + '">' + (idx + 1) + '/' + frames.length + '</span>'
1699
+ + '</div>'
1700
+ + '<div class="replay-info" id="replay-info-' + esc(id) + '">' + frameInfoHtml(f) + '</div>'
1701
+ + '</div>';
1702
+ }
1703
+ function updateScrubDom(id) {
1704
+ var s = streamById(id);
1705
+ if (!s) return;
1706
+ var frames = replayFrames(s);
1707
+ if (!frames.length) return;
1708
+ var idx = scrubIndexOf(id, frames);
1709
+ var f = frames[idx];
1710
+ var img = document.getElementById("replay-img-" + id);
1711
+ if (img) img.setAttribute("src", runArtifactHref(f && f.src));
1712
+ var badge = document.getElementById("replay-badge-" + id);
1713
+ if (badge) badge.textContent = f ? f.redaction : "raw";
1714
+ var cnt = document.getElementById("replay-count-" + id);
1715
+ if (cnt) cnt.textContent = (idx + 1) + "/" + frames.length;
1716
+ var info = document.getElementById("replay-info-" + id);
1717
+ if (info) info.innerHTML = frameInfoHtml(f);
1718
+ var range = app.querySelector('.replay-range[data-stream="' + id + '"]');
1719
+ if (range && String(range.value) !== String(idx)) range.value = idx;
1720
+ }
1721
+ function syncPlayBtn(id) {
1722
+ var btn = document.getElementById("replay-play-" + id);
1723
+ if (btn) btn.innerHTML = icon(scrubTimers[id] ? "pause" : "play", 13);
1724
+ }
1725
+ function stopScrubPlay(id) {
1726
+ if (scrubTimers[id]) { clearInterval(scrubTimers[id]); delete scrubTimers[id]; syncPlayBtn(id); }
1727
+ }
1728
+ function stopAllScrubTimers() {
1729
+ for (var k in scrubTimers) { if (scrubTimers.hasOwnProperty(k)) clearInterval(scrubTimers[k]); }
1730
+ scrubTimers = {};
1731
+ }
1732
+ function toggleScrubPlay(id) {
1733
+ if (scrubTimers[id]) { stopScrubPlay(id); return; }
1734
+ var s = streamById(id);
1735
+ var frames = s ? replayFrames(s) : [];
1736
+ if (frames.length < 2) return;
1737
+ if (scrubIndexOf(id, frames) >= frames.length - 1) { scrubState[id] = 0; updateScrubDom(id); } // restart from the top
1738
+ scrubTimers[id] = setInterval(function () {
1739
+ var st = streamById(id);
1740
+ var fr = st ? replayFrames(st) : [];
1741
+ if (fr.length < 2) { stopScrubPlay(id); return; }
1742
+ var cur = scrubIndexOf(id, fr);
1743
+ if (cur >= fr.length - 1) { stopScrubPlay(id); return; }
1744
+ scrubState[id] = cur + 1;
1745
+ updateScrubDom(id);
1746
+ }, 850);
1747
+ syncPlayBtn(id);
1748
+ }
1749
+ function handleScrub(id, op) {
1750
+ var s = streamById(id);
1751
+ var frames = s ? replayFrames(s) : [];
1752
+ if (!frames.length) return;
1753
+ if (op === "play") { toggleScrubPlay(id); return; }
1754
+ stopScrubPlay(id);
1755
+ var idx = scrubIndexOf(id, frames) + Number(op);
1756
+ if (idx < 0) idx = 0;
1757
+ if (idx > frames.length - 1) idx = frames.length - 1;
1758
+ scrubState[id] = idx;
1759
+ updateScrubDom(id);
1760
+ }
1608
1761
  function browserSurface(s, focus) {
1609
1762
  var live = tone(s.status) === "running";
1610
1763
  var route = laneRoute(s) || "(local)";
1611
1764
  var liveUrl = browserLiveUrl(s);
1612
1765
  var shot = browserShot(s);
1613
1766
  var dock = focus ? browserLabDock(s, shot) : "";
1767
+ var showReplay = focus && !(liveUrl && S.media === "live") && replayFrames(s).length >= 2;
1614
1768
  var body;
1615
1769
  if (liveUrl && S.media === "live") body = liveStreamMount(s, liveUrl);
1770
+ else if (showReplay) body = replayScrubber(s);
1616
1771
  else if (shot) body = '<img class="surface-fill surface-screenshot" src="' + esc(shot) + '" alt="' + (s.desktopGeometry ? "desktop screenshot" : "browser screenshot") + '"/>';
1617
1772
  else body = '<div class="bw-app-wait"><div class="wait-spinner" style="width:24px;height:24px"></div>'
1618
1773
  + '<div class="mono" style="font-size:9px">' + esc(route) + '</div>'
@@ -2495,6 +2650,7 @@ export function observerClientJs() {
2495
2650
  }
2496
2651
 
2497
2652
  function render() {
2653
+ stopAllScrubTimers(); // a full rebuild replaces the scrubber DOM; never leave a timer ticking on stale nodes
2498
2654
  var docEl = document.documentElement;
2499
2655
  docEl.setAttribute("data-theme", S.theme);
2500
2656
  docEl.setAttribute("data-motion", S.motion);
@@ -2561,6 +2717,7 @@ export function observerClientJs() {
2561
2717
  var arg = parts[1];
2562
2718
  var arg2 = parts[2];
2563
2719
  switch (cmd) {
2720
+ case "scrub": handleScrub(arg, arg2); break;
2564
2721
  case "open": openFocus(arg); break;
2565
2722
  case "select": S.focusedId = arg; writeHash(arg); render(); break;
2566
2723
  case "exit-focus": exitFocus(); break;
@@ -2615,7 +2772,16 @@ export function observerClientJs() {
2615
2772
  });
2616
2773
  app.addEventListener("input", function (e) {
2617
2774
  var t = e.target;
2618
- if (t && t.getAttribute && t.getAttribute("data-role") === "search") { S.query = t.value; render(); }
2775
+ if (!t || !t.getAttribute) return;
2776
+ if (t.getAttribute("data-role") === "search") { S.query = t.value; render(); return; }
2777
+ if (t.getAttribute("data-role") === "scrub") {
2778
+ // Direct DOM update (NOT a full render) so dragging the slider stays smooth and the slider
2779
+ // element is not rebuilt mid-drag. Position is stashed in scrubState so a later render restores it.
2780
+ var sid = t.getAttribute("data-stream");
2781
+ stopScrubPlay(sid);
2782
+ scrubState[sid] = Number(t.value);
2783
+ updateScrubDom(sid);
2784
+ }
2619
2785
  });
2620
2786
  document.addEventListener("mousedown", function (e) {
2621
2787
  if (openDd) {
@@ -1 +1 @@
1
- {"version":3,"file":"observer-assets.js","sourceRoot":"","sources":["../src/observer-assets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"observer-assets.js","sourceRoot":"","sources":["../src/observer-assets.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAC3E,gFAAgF;AAChF,+EAA+E;AAC/E,iEAAiE;AACjE,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,+CAA+C;AAE/C,MAAM,UAAU,WAAW;IACzB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsjCR,CAAC;AACF,CAAC;AAED,MAAM,UAAU,gBAAgB;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmxDR,CAAC;AACF,CAAC"}
@@ -3,7 +3,7 @@
3
3
  Date: 2026-06-02 (current-state note updated 2026-07-14)
4
4
 
5
5
  Status: reference map for the major contracts shipped through source version
6
- `0.20.3`; it is not an exhaustive inventory of command/result envelopes. Exported types,
6
+ `0.20.4`; it is not an exhaustive inventory of command/result envelopes. Exported types,
7
7
  schema constants, parsers, and validators in `src/` are authoritative. Rows
8
8
  marked "reserved" name layering intent only — no code emits or validates them
9
9
  yet. Do not emit a reserved schema.
@@ -16,7 +16,7 @@ Humanish should be the open-source CLI that lets a maintainer ask:
16
16
  The answer should be observable, verifiable, public-safe, and easy to turn into
17
17
  actionable feedback.
18
18
 
19
- ## Current Program Truth (source `0.20.3`)
19
+ ## Current Program Truth (source `0.20.4`)
20
20
 
21
21
  The package source and repository implementation in this tree agree on these
22
22
  points:
@@ -2,7 +2,7 @@
2
2
 
3
3
  Status: public-safe contributor and agent ramp.
4
4
 
5
- Package/source version in this tree: `0.20.3` (2026-08-03). The containment boundary introduced in
5
+ Package/source version in this tree: `0.20.4` (2026-08-03). The containment boundary introduced in
6
6
  `0.15.1` remains in force: managed run and output paths bind to validated
7
7
  physical filesystem identities, and stored provider IDs are evidence, not
8
8
  cleanup authority. The bundled OSS meta-lab is dry-run only until
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
4
4
  "description": "Open-source-safe CLI for persona simulation, observer review, and public-safe feedback drafts.",
5
5
  "author": "Daniel G Wilson <daniel@danielgwilson.com>",
6
6
  "keywords": [