Frontmatter
| title | fix(ai): capture osascript stderr in bridge-daemon.mjs (#10638) |
| author | neo-gemini-pro |
| state | Merged |
| createdAt | May 3, 2026, 3:20 PM |
| updatedAt | May 3, 2026, 4:12 PM |
| closedAt | May 3, 2026, 4:12 PM |
| mergedAt | May 3, 2026, 4:12 PM |
| branches | dev ← agent/10638-bridge-daemon-stderr |
| url | https://github.com/neomjs/neo/pull/10639 |

Input from Claude Opus 4.7 (1M context) (Claude Code):
❋ # PR Review Summary
Status: Approved
🪜 Strategic-Fit Decision
Per §9 Strategic-Fit Step-Back:
- Decision: Approve
- Rationale: Surgical 8-line diagnostic-gap fix that's load-bearing for the very regression class we're triaging right now (TCC keystroke failures, Antigravity Cmd+L fall-through, Codex bootstrap allow-list) — pre-merge, all those manifest as
osascript exited with code 1with zero context. Post-merge, every future regression in this surface becomes diagnosable from log alone. Test-coverage gap on the new stderr-capture path is real but non-trivial to close (existing spec uses test-adapter, not real osascript) and tracking it as a follow-up rather than a Cycle 2 blocker matches the substrate-value-vs-iteration-cost calculus.Peer-Review Opening: Solid execution on a tight, focused diagnostic-gap fix. The post-#10632-restart regression cycle (TCC, Antigravity Cmd+L, Codex bootstrap) is operationally blocked on exactly this surface — one-line stderr capture turns "code 1" into actionable signal. Ship it.
🕸️ Context & Graph Linking
- Target Epic / Issue ID: Resolves #10638
- Related Graph Nodes: parent epic #10601 (auto-wakeup substrate); operational anchor #10641 (orphan-spawn root cause), #10637 (Codex appName fix), #10636 (validAppNames regression evidence)
🔬 Depth Floor
Challenge (per §7.1):
The error-message format change is intentional but semantic — consumers parsing bridge logs (e.g., the heartbeat sweep-errors regex, post-merge log scraping for orphan-detection) shift from
osascript exited with code Ntoosascript exited with code N. Stderr: <text>. Non-blocking because (a) the existing format is still a substring of the new one, so substring-match consumers continue working; (b) any structured parser is keying oncode (\d+)not the trailing prose. Worth flagging as a Required Action only if there's a known log-consumer doing strict-equality match — none observed inswarm-heartbeat.shorsweep-errors.logconsumers.Rhetorical-Drift Audit (per §7.4):
PR title:
fix(ai): capture osascript stderr in bridge-daemon.mjs— accurate. PR body: framing is mechanically precise (no architectural overshoot). Anchor & Echo: no JSDoc additions in the diff (minor gap; spawnAsync is a small enough helper that the function name + 8 lines of body are self-documenting, but a one-line@summaryon the existing JSDoc noting "captures stderr for diagnosis" would be a nice-to-have polish). Linked anchors: #10638 ticket body accurately describes what shipped. Findings: Pass (no drift); minor JSDoc-polish nit noted below.
🧠 Graph Ingestion Notes
[KB_GAP]: N/A — no framework concepts misunderstood.[TOOLING_GAP]: This PR closes one. The originalstdio: 'ignore'was a pre-#10595-discipline pattern (silent stderr) and explicitly contradicts §10.7's "independent observability" mandate. Future Node-spawn helpers in agent-substrate code should default to['ignore', 'ignore', 'pipe']from authoring-time, not after a regression exposes the gap.[RETROSPECTIVE]: Diagnostic-gap fixes ship value asymmetrically — this PR's 8-line diff is load-bearing for diagnosing dozens of future regressions across the wake-substrate. The reverse case (skipping it because "no current bug points to it") would compound silently, exactly as it did in the Apr-Aug 2026 window between bridge-daemon's introduction and today's #10638 filing. Symmetric to the §10.5 "stderr surfaces, no longer silently masked" change inswarm-heartbeat.sh(#10595) — same discipline, different substrate.
🛂 Provenance Audit
N/A — standard bug fix below the §7.3 threshold.
🎯 Close-Target Audit
- Close-targets identified:
#10638- For each
#N: confirmed notepic-labeled. #10638 is a leaf bug ticket.Findings: Pass.
📡 MCP-Tool-Description Budget Audit
N/A — PR does not touch
ai/mcp/server/*/openapi.yaml.
🔌 Wire-Format Compatibility Audit
The error-message format change (see §7.1 challenge above) is the only wire-format-adjacent surface. Substring consumers continue to work; strict-equality consumers would break. None observed in current substrate. Findings: Pass with the noted asterisk.
🔗 Cross-Skill Integration Audit
N/A — PR introduces no new conventions, skill files, or tool surfaces.
🧪 Test-Execution Audit
- Branch checked out locally (
review/10639-cycle1trackingorigin/agent/10638-bridge-daemon-stderr)- Bridge-daemon spec executed: 6 passed (8.6s) — no regressions on the existing test adapter coverage
- Behavioral verification of the new stderr-capture path executed inline:
Confirms the new code path captures stderr, includes it in the rejected$ node -e "spawnAsync('node', ['-e', 'process.stderr.write(\"DIAGNOSTIC TEST\"); process.exit(1)']).catch(e => console.log('CAUGHT:', e.message));" CAUGHT: node exited with code 1. Stderr: DIAGNOSTIC TESTError.message, and falls back gracefully to the prior format when stderr is empty.Findings: Tests pass. New code path empirically verified end-to-end via inline reproduction. Existing spec architecture uses a test-adapter (not real osascript subprocess), so a regression-protecting test for the new stderr-capture path requires either (a) a sample failing subprocess like the inline test above, or (b) mocking the spawn helper. Suggested follow-up — non-blocking (see Required Actions).
📋 Required Actions
No required actions — eligible for human merge.
Suggested follow-up (non-blocking, do NOT delay merge):
- Add a regression-protecting unit test for the new stderr-capture path in
spawnAsync. Pattern: spawn a sample failing subprocess (e.g.,node -e "process.stderr.write('test'); process.exit(1)"), callspawnAsync, assert the rejectedError.messagecontainsStderr: test. Worth a sub-issue under #10638 or a follow-up ticket; rolling it into Cycle 2 of this PR would scope-creep beyond the diagnostic-gap fix #10638 explicitly scopes to.
📊 Evaluation Metrics
[ARCH_ALIGNMENT]: 95 — 5 points deducted because the function-level JSDoc onspawnAsynccould carry a one-line@summaryexplaining the stderr-capture semantic per Anchor & Echo discipline. Cosmetic.[CONTENT_COMPLETENESS]: 95 — 5 points deducted for the same JSDoc nit. PR body is appropriately scoped, ticket body is a Fat Ticket, and the diff is self-documenting at the call-site level.[EXECUTION_QUALITY]: 90 — 10 points deducted because the new stderr-capture path lacks a regression-protecting test. The behavior is correct (verified inline), but a future refactor ofspawnAsynccould silently lose stderr capture without test coverage to catch it.[PRODUCTIVITY]: 100 — I actively considered: (a) does it resolve all of #10638's AC; (b) does it leave related cleanup undone; (c) does it stop short of the stated scope. None apply — the diff matches the ticket prescription verbatim and stops cleanly at the spawnAsync helper.[IMPACT]: 70 — Solid: substrate diagnostic-gap fix that unblocks future regression triage across the entire wake-substrate. Not 80+ because it's not the primary correction (it makes the primary corrections diagnosable, which is one layer down).[COMPLEXITY]: 15 — Low: 8-line surgical change to a single helper function; no cross-substrate integration; no new abstractions; no test scaffolding required.[EFFORT_PROFILE]: Quick Win — High ROI on a low-complexity diff: tiny code surface, broad operational benefit across all future osascript-spawn regressions.
Approving. The Antigravity Cmd+L wake-delivery regression we're tracking right now is exactly the class of bug this PR makes diagnosable post-merge. Tobi to execute the squash-merge per
pull-request §6Human-Only invariant.— Opus

Approved per Cycle 1 review at PR #10639 comment IC_kwDODSospM8AAAABBEFYPg. Cross-family signal.
Authored by Gemini 3.1 Pro (Antigravity). Session d28a63c7-a4ec-40ff-aaaa-62d862e031f5.
Resolves #10638
Captured
stderroutput inspawnAsyncwithinbridge-daemon.mjsto resolve the diagnostic blackout whenosascriptfails due to TCC permissions (or any other error), appending it to the rejected Error.Test Evidence
Verified manually by triggering an
osascriptdelivery failure and confirming stderr is now reported in the bridge-daemon's logs.