@zhuxixi/pi-agent-board 0.5.1 → 0.6.0

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +41 -3
  3. package/docs/superpowers/plans/2026-09-01-attach-detach-gate-cursor-anchor.md +284 -0
  4. package/docs/superpowers/plans/2026-09-02-attach-detach-editor-state.md +722 -0
  5. package/docs/superpowers/plans/2026-09-03-code-refs-pr-backlink-narrow.md +551 -0
  6. package/docs/superpowers/plans/2026-09-03-detach-gate-glyph-fallback.md +146 -0
  7. package/docs/superpowers/plans/2026-09-04-evidence-outputpreview.md +209 -0
  8. package/docs/superpowers/plans/2026-09-04-warm-host-reclaim.md +796 -0
  9. package/docs/superpowers/plans/2026-09-05-issue-13-drainnextfollowup-pty-probe.md +114 -0
  10. package/docs/superpowers/plans/2026-09-05-issue-38-windows-wezterm-ime-cursor.md +73 -0
  11. package/docs/superpowers/plans/2026-09-05-issue-39-truncate-codepoint-boundary.md +143 -0
  12. package/docs/superpowers/plans/2026-09-05-issue-61-mention-fallback-guards.md +226 -0
  13. package/docs/superpowers/plans/2026-09-05-issue-63-flaky-manual-completion.md +87 -0
  14. package/docs/superpowers/plans/2026-09-05-issue-64-changelog-release-helper.md +53 -0
  15. package/docs/superpowers/plans/2026-09-05-pty-host-stacking-sock-race.md +731 -0
  16. package/docs/superpowers/specs/2026-08-29-code-refs-badges-design.md +1 -1
  17. package/docs/superpowers/specs/2026-09-01-attach-detach-gate-cursor-anchor-design.md +78 -0
  18. package/docs/superpowers/specs/2026-09-02-attach-detach-editor-state-design.md +120 -0
  19. package/docs/superpowers/specs/2026-09-03-code-refs-pr-backlink-narrow-design.md +92 -0
  20. package/docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md +99 -0
  21. package/docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md +50 -0
  22. package/docs/superpowers/specs/2026-09-04-warm-host-reclaim-design.md +106 -0
  23. package/docs/superpowers/specs/2026-09-05-issue-13-drainnextfollowup-pty-probe-design.md +64 -0
  24. package/docs/superpowers/specs/2026-09-05-issue-38-windows-wezterm-ime-design.md +48 -0
  25. package/docs/superpowers/specs/2026-09-05-issue-39-truncate-codepoint-boundary-design.md +64 -0
  26. package/docs/superpowers/specs/2026-09-05-issue-61-mention-fallback-design.md +71 -0
  27. package/docs/superpowers/specs/2026-09-05-issue-63-flaky-manual-completion-design.md +49 -0
  28. package/docs/superpowers/specs/2026-09-05-issue-64-changelog-helper-design.md +76 -0
  29. package/docs/superpowers/specs/2026-09-05-pty-host-stacking-sock-race-design.md +510 -0
  30. package/package.json +83 -81
  31. package/runner/job-runner.mjs +2 -2
  32. package/runner/pty-runner.mjs +585 -4
  33. package/runner/state-runner.mjs +3 -0
  34. package/runner/title-runner.mjs +1 -1
  35. package/scripts/release_helper.mjs +277 -0
  36. package/src/commands/agent-board.ts +38 -35
  37. package/src/commands/attach-decision.mjs +66 -0
  38. package/src/commands/attach-flow.ts +45 -39
  39. package/src/core/code-refs.mjs +85 -33
  40. package/src/core/editor-state-reporter.mjs +102 -0
  41. package/src/core/evidence.mjs +2 -2
  42. package/src/core/heuristics.mjs +40 -2
  43. package/src/core/host-coordination.mjs +159 -0
  44. package/src/core/host-crash.mjs +43 -3
  45. package/src/core/host-probe.mjs +196 -0
  46. package/src/core/launch.mjs +3 -1
  47. package/src/core/locks.mjs +196 -1
  48. package/src/core/paths.mjs +24 -0
  49. package/src/core/pty-input.mjs +32 -0
  50. package/src/core/store.mjs +164 -5
  51. package/src/core/types.mjs +17 -1
  52. package/src/core/warm-host-sweeper.mjs +150 -0
  53. package/src/index.ts +41 -2
  54. package/src/runtime/service.mjs +967 -109
  55. package/src/ui/dashboard-decisions.mjs +55 -0
  56. package/src/ui/dashboard.ts +26 -12
  57. package/src/ui/pty-attach.ts +55 -7
@@ -0,0 +1,146 @@
1
+ # Detach-Gate Tier-2 Glyph Fallback Tightening Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Tighten the attach-surface `←` detach gate's tier-2 glyph fallback so content glyph lines (markdown table rows / quotes / drafts) no longer trap the user when `editor_state` is unavailable (issue #69).
6
+
7
+ **Architecture:** One-line condition change in `PtyAttachComponent.childInputLooksEmpty()` (src/ui/pty-attach.ts): the tier-2 loop now only treats an EMPTY prompt-glyph line as proof of an empty editor (`isProbablyPiInputLine(line) && isProbablyEmptyPiInputLine(line)`), skipping content glyph lines; loop-end escape stays authoritative. Tier-1 (inverse fake-cursor anchor), the `editor_state` authoritative path (#71), and all protocol/public API surfaces are untouched. Behavior is pinned by two new detach-gate smoke scenarios (K1/K2) asserted inside the existing `test/pty-attach-detach-gate.test.mjs` case.
8
+
9
+ **Tech Stack:** TypeScript (Node `--experimental-transform-types`), node:test, @xterm/headless in-memory smoke harness.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-03-detach-gate-glyph-fallback-design.md` (accepted 2026-09-03, Option B).
12
+
13
+ **Acceptance traceability:** Task 1 → A1, A2, A3, A4 (automated). Task 2 → U1, U2 (manual, human-executed after push, before merge). No orphan tasks, no orphan acceptance IDs.
14
+
15
+ ## Global Constraints
16
+
17
+ - Work only inside this worktree: `/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-69-detach-gate-glyph-fallback`. Never edit the main checkout at `/home/elling/git-repo/github/pi-agent-board` (`node_modules` there is only the read-only symlink target).
18
+ - Commit messages: English, conventional commits, suffix `(issue #69)`.
19
+ - Stage files explicitly (`git add <file> ...`); never `git add -A`.
20
+ - Do NOT touch: tier-1 inverse anchor, `editor_state` path (`resolveEditorEmpty`, reporter, runner), README.md, or the pure helpers in `src/core/pty-input.mjs` (spec non-goals).
21
+ - Do NOT extract new pure functions — the tightened tier-2 is constant-true by design; the discriminating tests live at smoke level (spec §4).
22
+ - Every new online-gate smoke scenario MUST pin `connected=true` explicitly before `handleInput` (B-section convention; otherwise the disconnected-escape semantics fire instead of the online gate).
23
+ - `node --test` case count stays 437: the new smoke keys are assertions inside the existing test case in `test/pty-attach-detach-gate.test.mjs`, not new `test()` blocks.
24
+ - Verification commands: `npm test` (expect 437 pass, 0 fail) and `npm run typecheck` (expect 0 errors). Faster smoke-only loop: `node --experimental-transform-types test-support/detach-gate-smoke.ts` (prints a JSON of scenario keys).
25
+
26
+ ---
27
+
28
+ ### Task 1: Tighten tier-2 glyph fallback (TDD) — covers A1, A2, A3, A4
29
+
30
+ **Files:**
31
+ - Modify: `src/ui/pty-attach.ts` (the tier-2 loop inside `childInputLooksEmpty()`, right after the `// Fallback: Pi variants that render no fake cursor` comment, line 359)
32
+ - Test: `test-support/detach-gate-smoke.ts` (insert two scenarios after the J-scenario block, before the `// E2.` comment)
33
+ - Test: `test/pty-attach-detach-gate.test.mjs` (two assertions after the `leftHelloNullResetsStaleEditorState` assertion)
34
+
35
+ **Interfaces:**
36
+ - Consumes: `isProbablyPiInputLine(line)`, `isProbablyEmptyPiInputLine(line)` from `src/core/pty-input.mjs` (both already imported in `pty-attach.ts`); smoke harness helpers `makeAttach()`, `writeToTerm(attach, data)` (already defined in the smoke file).
37
+ - Produces: smoke output keys `leftDetachesOnTableRowsWithoutFakeCursor` (A1) and `leftDetachesOnContentGlyphFallback` (A2) in the JSON printed by `test-support/detach-gate-smoke.ts`.
38
+
39
+ - [ ] **Step 1: Write the failing smoke scenarios (A1, A2)**
40
+
41
+ In `test-support/detach-gate-smoke.ts`, insert these two blocks immediately BEFORE the line starting with `// E2. A terminal at the minimum supported size`:
42
+
43
+ ```ts
44
+ // K1. Issue #69 real-world shape: zero inverse cells anywhere in the buffer,
45
+ // the chat area carries a markdown table row (`│ … │`) and a quote line
46
+ // (`> …`) that isProbablyPiInputLine misreads as a draft-bearing input line,
47
+ // and editor_state never arrives (editorEmpty stays null — child without the
48
+ // reporter). tier-2 must skip content glyph lines and ← must detach: the
49
+ // gate philosophy is "never trap the user" (issues #42/#48).
50
+ {
51
+ const { attach, sent, didDetach } = makeAttach();
52
+ await writeToTerm(attach, "chat content\r\n│ Issue #778 │ open │\r\n> quote line\r\n");
53
+ (attach as unknown as { connected: boolean }).connected = true;
54
+ attach.handleInput("\x1b[D");
55
+ out.leftDetachesOnTableRowsWithoutFakeCursor = didDetach() && sent.length === 1 && sent[0].type === "detach";
56
+ attach.dispose();
57
+ }
58
+
59
+ // K2. The deliberate flip side of K1 — pair with scenario B: the SAME draft
60
+ // shape (`> draft`) is gated when the fake cursor is present (tier-1, scenario
61
+ // B) but detaches when the buffer carries no inverse cells (tier-2 fallback
62
+ // cannot tell a real draft from a table row; a spurious detach beats a trapped
63
+ // user, and detach never loses the draft — the child session keeps running).
64
+ // This pins the intentional loss of fallback draft protection (issue #69);
65
+ // restoring it needs the mid-term dock-structure anchor, not a revert.
66
+ {
67
+ const { attach, sent, didDetach } = makeAttach();
68
+ await writeToTerm(attach, "chat content\r\n> draft\r\n");
69
+ (attach as unknown as { connected: boolean }).connected = true;
70
+ attach.handleInput("\x1b[D");
71
+ out.leftDetachesOnContentGlyphFallback = didDetach() && sent.length === 1 && sent[0].type === "detach";
72
+ attach.dispose();
73
+ }
74
+ ```
75
+
76
+ In `test/pty-attach-detach-gate.test.mjs`, insert these two assertions immediately AFTER the `assert.equal(parsed.leftHelloNullResetsStaleEditorState, true, ...)` line:
77
+
78
+ ```js
79
+ assert.equal(parsed.leftDetachesOnTableRowsWithoutFakeCursor, true, "← must detach when a zero-inverse buffer holds only table/quote glyph lines and editor_state is unknown (issue #69)");
80
+ assert.equal(parsed.leftDetachesOnContentGlyphFallback, true, "← must detach on a content glyph line in the no-fake-cursor fallback — spurious detach beats trapping (issue #69)");
81
+ ```
82
+
83
+ - [ ] **Step 2: Run to verify both fail (A1, A2 red)**
84
+
85
+ Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
86
+ Expected: JSON contains `"leftDetachesOnTableRowsWithoutFakeCursor":false` and `"leftDetachesOnContentGlyphFallback":false` (all pre-existing keys stay `true`).
87
+
88
+ - [ ] **Step 3: Implement the tier-2 tightening (minimal change)**
89
+
90
+ In `src/ui/pty-attach.ts`, inside `childInputLooksEmpty()`, replace this exact block:
91
+
92
+ ```ts
93
+ // Fallback: Pi variants that render no fake cursor — look for a
94
+ // prompt-glyph line.
95
+ for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
96
+ const line = active.getLine(y)?.translateToString(true) ?? "";
97
+ if (isProbablyPiInputLine(line)) return isProbablyEmptyPiInputLine(line);
98
+ }
99
+ ```
100
+
101
+ with:
102
+
103
+ ```ts
104
+ // Fallback: Pi variants that render no fake cursor — look for an EMPTY
105
+ // prompt-glyph line. Only an empty glyph line proves an empty editor:
106
+ // content glyph lines (markdown table rows `│ … │`, quotes `> …`, or a
107
+ // real draft in a no-fake-cursor Pi variant) cannot be told apart, and
108
+ // trapping the user is worse than a spurious detach (issue #69) — skip
109
+ // them and keep scanning; the loop-end escape below stays authoritative.
110
+ for (let y = active.baseY + active.length - 1; y >= active.baseY; y--) {
111
+ const line = active.getLine(y)?.translateToString(true) ?? "";
112
+ if (isProbablyPiInputLine(line) && isProbablyEmptyPiInputLine(line)) return true;
113
+ }
114
+ ```
115
+
116
+ - [ ] **Step 4: Run smoke to verify the fix (A1, A2 green)**
117
+
118
+ Run: `node --experimental-transform-types test-support/detach-gate-smoke.ts`
119
+ Expected: EVERY key in the JSON is `true`, including the two new ones.
120
+
121
+ - [ ] **Step 5: Full automated acceptance (A3, A4)**
122
+
123
+ Run: `npm test`
124
+ Expected: `pass 437`, `fail 0` (count unchanged — the new keys are assertions inside the existing test case).
125
+
126
+ Run: `npm run typecheck`
127
+ Expected: exit 0, no output errors.
128
+
129
+ - [ ] **Step 6: Commit**
130
+
131
+ ```bash
132
+ git add src/ui/pty-attach.ts test-support/detach-gate-smoke.ts test/pty-attach-detach-gate.test.mjs
133
+ git commit -m "fix: tighten ← detach tier-2 glyph fallback to empty-only lines (issue #69)"
134
+ ```
135
+
136
+ ### Task 2: Manual verification U1/U2 (human-executed; after PR branch is pushed, before merge) — covers U1, U2
137
+
138
+ > Not a subagent task. The controller surfaces this checklist to the human partner at the pre-merge gate.
139
+
140
+ **Environment:** the running extension copy is the git-source clone at `~/.pi/agent/git/github.com/zhuxixi/pi-agent-board` (currently at 0.5.1 / fac9e91). Steps:
141
+
142
+ - [ ] **Step 1:** `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board fetch origin && git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board checkout issue-69-detach-gate-glyph-fallback`
143
+ - [ ] **Step 2:** Restart pi IN ANOTHER TERMINAL (restarting kills the session running this agent — do not run inside the controlling session's pi instance).
144
+ - [ ] **Step 3 (U1):** Open dashboard → attach a warm session whose chat area contains markdown table output (`│ … │` rows) → with an empty input box press `←`. Pass = returns to dashboard, not trapped.
145
+ - [ ] **Step 4 (U2):** In the same or another reporter-active hosted session, type a draft → press `←`. Pass = cursor moves left inside the draft, no detach (same as #67 U3).
146
+ - [ ] **Step 5:** `git -C ~/.pi/agent/git/github.com/zhuxixi/pi-agent-board checkout main` and restart pi to restore the running copy.
@@ -0,0 +1,209 @@
1
+ # Evidence outputPreview Extraction Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Fix `evidence.json` `commands[].outputPreview` storing `[object Object]` for bash tool results by extracting text from pi's `AgentToolResult.content` blocks (issue #41).
6
+
7
+ **Architecture:** New pure function `toolResultText(result)` in `src/core/heuristics.mjs` (symmetric with existing `assistantText`); `reduceEvidence` bash branch swaps `String(event.result ?? "")` for it. No data-structure changes, no migration.
8
+
9
+ **Tech Stack:** Node.js ESM, `node:test`, JSDoc types checked by `tsc --noEmit`.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-09-04-evidence-outputpreview-design.md` (acceptance IDs A1/A2/A3/U1 referenced below).
12
+
13
+ ## Global Constraints
14
+
15
+ - All edits under worktree `$WT=/home/elling/git-repo/github/pi-agent-board/.pi/worktrees/issue-41-evidence-outputpreview-object`; git via `git -C $WT` — never touch main checkout.
16
+ - `toolResultText` must stay a pure function with zero side effects; do NOT inline it into `reduceEvidence` (spec §3 hard constraint).
17
+ - Unknown-shaped objects must yield `""`, never `"[object Object]"` (spec D2, #40's 宁可不显示也不错显示 principle).
18
+ - Legacy string `result` must keep passing through unchanged (spec D5).
19
+ - Do NOT change the 500-char truncate, `EvidenceCommand` shape, other tools' branches, or powershell handling (spec §5 non-goals).
20
+ - Stage files individually with `git -C $WT add <file>`; never `git add -A`.
21
+ - Full gate per task end: `npm run verify` (typecheck + test + coverage + pack:dry). Never `npm test -- <file>` (glob expands to full suite anyway).
22
+
23
+ ---
24
+
25
+ ### Task 1: `toolResultText` pure function (acceptance: A1)
26
+
27
+ **Files:**
28
+ - Modify: `src/core/heuristics.mjs` (after `assistantText`, line ~26)
29
+ - Test: `test/heuristics.test.mjs`
30
+
31
+ **Interfaces:**
32
+ - Produces: `toolResultText(result: any): string` — exported from `src/core/heuristics.mjs`. Task 2 imports it in `src/core/evidence.mjs`.
33
+ - Behavior table (spec D2): `null/undefined → ""`; `string → passthrough`; object with `content` array → text blocks `join("\n")` trimmed; object without content/text → `""`; number/bool → `String()`.
34
+
35
+ - [ ] **Step 1: Write the failing tests**
36
+
37
+ Append to `test/heuristics.test.mjs`, and add `toolResultText` to the existing import block from `../src/core/heuristics.mjs`:
38
+
39
+ ```js
40
+ test("toolResultText extracts text blocks from AgentToolResult", () => {
41
+ const result = {
42
+ content: [
43
+ { type: "text", text: "line1" },
44
+ { type: "image", data: "AAAA", mimeType: "image/png" },
45
+ { type: "text", text: "line2" },
46
+ ],
47
+ details: { fullOutputPath: "/tmp/pi-bash-x.log" },
48
+ };
49
+ assert.equal(toolResultText(result), "line1\nline2");
50
+ });
51
+
52
+ test("toolResultText returns empty string for image-only content", () => {
53
+ assert.equal(toolResultText({ content: [{ type: "image", data: "AAAA", mimeType: "image/png" }] }), "");
54
+ });
55
+
56
+ test("toolResultText returns empty string for object without content array", () => {
57
+ assert.equal(toolResultText({ details: {} }), "");
58
+ assert.equal(toolResultText({}), "");
59
+ });
60
+
61
+ test("toolResultText passes through plain strings (legacy events)", () => {
62
+ assert.equal(toolResultText("ok"), "ok");
63
+ });
64
+
65
+ test("toolResultText handles nullish and scalar inputs", () => {
66
+ assert.equal(toolResultText(null), "");
67
+ assert.equal(toolResultText(undefined), "");
68
+ assert.equal(toolResultText(42), "42");
69
+ assert.equal(toolResultText(true), "true");
70
+ });
71
+ ```
72
+
73
+ - [ ] **Step 2: Run tests to verify they fail**
74
+
75
+ Run: `cd $WT && node --test test/heuristics.test.mjs`
76
+ Expected: FAIL — `toolResultText is not defined` / import error.
77
+
78
+ - [ ] **Step 3: Implement `toolResultText` in `src/core/heuristics.mjs`**
79
+
80
+ Insert after the existing `assistantText` function:
81
+
82
+ ```js
83
+ /**
84
+ * Extract text from a pi AgentToolResult object
85
+ * ({ content: [{ type: "text", text }, ...], details, ... }), or, defensively,
86
+ * a plain string. Unknown-shaped objects yield "" — never "[object Object]"
87
+ * (issue #41; mirrors pi's own convertToolResultOutput join("\n") semantics).
88
+ * @param {any} result
89
+ * @returns {string}
90
+ */
91
+ export function toolResultText(result) {
92
+ if (result == null) return "";
93
+ if (typeof result === "string") return result;
94
+ if (typeof result !== "object") return String(result);
95
+ const content = result.content;
96
+ if (!Array.isArray(content)) return "";
97
+ return content
98
+ .filter((b) => b && b.type === "text" && typeof b.text === "string")
99
+ .map((b) => b.text)
100
+ .join("\n")
101
+ .trim();
102
+ }
103
+ ```
104
+
105
+ - [ ] **Step 4: Run tests to verify they pass**
106
+
107
+ Run: `cd $WT && node --test test/heuristics.test.mjs`
108
+ Expected: PASS (all 5 new tests + pre-existing suite).
109
+
110
+ - [ ] **Step 5: Commit**
111
+
112
+ ```bash
113
+ git -C $WT add test/heuristics.test.mjs src/core/heuristics.mjs
114
+ git -C $WT commit -m "feat: add toolResultText extractor for AgentToolResult content (issue #41)"
115
+ ```
116
+
117
+ ---
118
+
119
+ ### Task 2: `reduceEvidence` bash branch uses `toolResultText` (acceptance: A2, A3)
120
+
121
+ **Files:**
122
+ - Modify: `src/core/evidence.mjs` (import line 2; bash branch line ~196)
123
+ - Test: `test/evidence.test.mjs`
124
+
125
+ **Interfaces:**
126
+ - Consumes: `toolResultText(result: any): string` from Task 1.
127
+ - Produces: unchanged `EvidenceCommand` shape; `outputPreview` now carries extracted text.
128
+
129
+ - [ ] **Step 1: Write the failing test (real AgentToolResult shape)**
130
+
131
+ In `test/evidence.test.mjs`, inside the existing top-level test that builds `snap` (after the current `b1` end event at line ~24), append:
132
+
133
+ ```js
134
+ reduceEvidence(snap, { type: "tool_execution_end", toolCallId: "b2", toolName: "bash", isError: false, result: { content: [{ type: "text", text: "https://github.com/zhuxixi/pi-agent-board/pull/45" }] } }, 31);
135
+ ```
136
+
137
+ and alongside the existing assertions:
138
+
139
+ ```js
140
+ assert.equal(snap.commands.find((c) => c.id === "b2").outputPreview, "https://github.com/zhuxixi/pi-agent-board/pull/45");
141
+ ```
142
+
143
+ (The pre-existing `b1` fixture with `result: "ok"` stays untouched — its `outputPreview` passthrough is the D5 regression guard.)
144
+
145
+ **Assertion ripple:** the new `b2` command raises the command count. In the same test, update the tail assertion:
146
+
147
+ ```js
148
+ assert.equal(summary.commandCount, 2);
149
+ ```
150
+
151
+ (was `assert.equal(summary.commandCount, 1);`)
152
+
153
+ - [ ] **Step 2: Run test to verify it fails**
154
+
155
+ Run: `cd $WT && node --test test/evidence.test.mjs`
156
+ Expected: FAIL — `outputPreview` is `"[object Object]"` instead of the URL.
157
+
158
+ - [ ] **Step 3: Modify `src/core/evidence.mjs`**
159
+
160
+ Import line (currently):
161
+
162
+ ```js
163
+ import { assistantText, classifyCommand, toolFileOperation, truncate } from "./heuristics.mjs";
164
+ ```
165
+
166
+ becomes:
167
+
168
+ ```js
169
+ import { assistantText, classifyCommand, toolFileOperation, toolResultText, truncate } from "./heuristics.mjs";
170
+ ```
171
+
172
+ In `reduceEvidence`, `tool_execution_end` → `name === "bash"` branch, replace exactly this line:
173
+
174
+ ```js
175
+ outputPreview: truncate(String(event.result ?? ""), 500),
176
+ ```
177
+
178
+ with:
179
+
180
+ ```js
181
+ outputPreview: truncate(toolResultText(event.result), 500),
182
+ ```
183
+
184
+ - [ ] **Step 4: Run tests to verify they pass**
185
+
186
+ Run: `cd $WT && node --test test/evidence.test.mjs`
187
+ Expected: PASS — new assertion green AND legacy `"ok"` fixture still green (D5).
188
+
189
+ - [ ] **Step 5: Full verify gate**
190
+
191
+ Run: `cd $WT && npm run verify`
192
+ Expected: PASS — typecheck, full test suite, coverage, pack:dry all green.
193
+
194
+ - [ ] **Step 6: Commit**
195
+
196
+ ```bash
197
+ git -C $WT add src/core/evidence.mjs test/evidence.test.mjs
198
+ git -C $WT commit -m "fix: extract evidence outputPreview from AgentToolResult content (issue #41)"
199
+ ```
200
+
201
+ ---
202
+
203
+ ### Task 3: Post-merge manual verification (acceptance: U1, user-verified)
204
+
205
+ **Trigger:** after PR merge + board reloads the new extension on next pi session start.
206
+
207
+ - [ ] **Step 1:** In a real pi session, run a few bash commands (e.g. `gh pr create`, `git log`).
208
+ - [ ] **Step 2:** Inspect `~/.pi/agent/agent-board/views/<view>/evidence.json`.
209
+ - [ ] **Step 3:** Pass criteria: newly recorded bash commands show real output text in `outputPreview` (e.g. the `gh pr create` URL); no new `[object Object]` entries. Record observation result back on issue #41.