thinkpool-pair 0.7.103 → 0.7.105

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.
package/bridge.mjs CHANGED
@@ -1322,6 +1322,11 @@ function openStructured({ id, model, resume, log, commands, mode, spawnedBy, rol
1322
1322
  })
1323
1323
  entry.session = startClaudeSession({
1324
1324
  cwd: cwd || process.cwd(), model, resume, mode, rolePrompt,
1325
+ // A Flow CONDUCTOR (flowSessionId set, no flowTaskKey) must DECOMPOSE, not explore via
1326
+ // a fan-out of Task/Explore subagents — those raise permission cards in plan mode (which
1327
+ // now land on the hidden conductor terminal) and the model ignores the prompt rule not
1328
+ // to. Hard-block the Task tool for conductors so it reads with Read/Grep/Glob + ExitPlanMode.
1329
+ blockSubagents: !!flowSessionId && !flowTaskKey,
1325
1330
  mcpServers: { thinkpool: peekServer },
1326
1331
  // Tier C precheck — the PreToolUse gate calls this BEFORE raising a card, so a
1327
1332
  // disabled/looping/over-cap post never bothers a person. Closes over `entry`.
@@ -1433,6 +1438,26 @@ function drainPending(s) {
1433
1438
  s.pending.clear()
1434
1439
  }
1435
1440
 
1441
+ // Switching a terminal to bypassPermissions must RETROACTIVELY clear the cards the
1442
+ // current turn already raised — setPermissionMode is a streaming control request that
1443
+ // only applies going forward, so without this a user who flips to bypass mid-turn keeps
1444
+ // staring at "the agent wants to run X" cards on a terminal that's supposedly hands-off
1445
+ // (the 2026-06-30 "AGENT STILL ASKING PERMISSIONS" report — incl. subagent/Explore tool
1446
+ // calls whose requestPermission was invoked before the toggle). bypass = allow-all, so
1447
+ // resolving every pending card as 'allow' is exactly what the new mode means.
1448
+ function allowPending(s) {
1449
+ if (!s?.pending) return
1450
+ for (const [key, p] of s.pending) {
1451
+ // Only auto-clear ordinary tool cards. A pending PLAN approval (risk:'plan') or an
1452
+ // AskUserQuestion (risk:'ask') is a deliberate human gate — flipping to bypass means
1453
+ // "run tools without asking", not "auto-approve my plan / answer my question" — leave
1454
+ // those for the human.
1455
+ if (p?.payload?.risk === 'plan' || p?.payload?.risk === 'ask') continue
1456
+ try { p.resolve('allow') } catch { /* noop */ }
1457
+ s.pending.delete(key)
1458
+ }
1459
+ }
1460
+
1436
1461
  function endStructured(id) {
1437
1462
  if (!id) return
1438
1463
  const s = sessions.get(id)
@@ -1712,7 +1737,11 @@ channel
1712
1737
  })
1713
1738
  .on('broadcast', { event: 'code-mode' }, ({ payload }) => {
1714
1739
  const s = payload?.term && sessions.get(payload.term)
1715
- if (s) { s.mode = payload.mode; s.session.setMode(payload.mode) } // track for persist/restore
1740
+ if (s) {
1741
+ s.mode = payload.mode; s.session.setMode(payload.mode) // track for persist/restore
1742
+ // Flipping to bypass = "stop asking, including the cards this turn already raised."
1743
+ if (payload.mode === 'bypassPermissions') allowPending(s)
1744
+ }
1716
1745
  })
1717
1746
  .on('broadcast', { event: 'code-close' }, ({ payload }) => {
1718
1747
  endStructured(payload?.id)
@@ -120,7 +120,7 @@ const simplifyBlocks = (blocks = []) => blocks.map((b) => {
120
120
  */
121
121
  const MODES = new Set(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
122
122
 
123
- export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt }) {
123
+ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode = 'default', onEvent, requestPermission, mcpServers, crossPostGate, crossRoomPostGate, rolePrompt, blockSubagents = false }) {
124
124
  const ac = new AbortController()
125
125
  let input = makeInputStream() // `let`: auto-restart swaps in a fresh stream
126
126
  let sessionId = resume || null
@@ -248,6 +248,15 @@ export function startClaudeSession({ cwd, model, resume, env, mode: initialMode
248
248
  // Surface it to the room when it changes; absent on models without effort.
249
249
  const lvl = hookInput.effort?.level
250
250
  if (lvl && lvl !== effort) { effort = lvl; emit({ kind: 'effort', level: effort }) }
251
+ // Flow CONDUCTOR hard-stop: a conductor must DECOMPOSE, never fan out Task/Explore
252
+ // subagents. The prompt asks it not to, but the model's parallelize instinct wins and
253
+ // it spawns Explore agents anyway — which (in plan mode) raise permission cards on the
254
+ // now-hidden conductor terminal and hang the whole flow. Deny the Task tool outright and
255
+ // tell it to read with Read/Grep/Glob + ExitPlanMode. (blockSubagents is set only for
256
+ // the conductor session — lanes are governed by their own one-hop prompt rule.)
257
+ if (blockSubagents && (toolName === 'Task' || toolName === 'Agent')) {
258
+ return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: 'Flow conductors do NOT spawn subagents — it stalls the flow on a permission card. If you must inspect the codebase, read it yourself with Read/Grep/Glob (allowed here, no prompt). Then DECOMPOSE and call ExitPlanMode once. Do not call Task/Agent again.' } }
259
+ }
251
260
  // ThinkPool cross-terminal READ (B2) is read-only + within-room — never
252
261
  // prompt, in any mode. It still surfaces as a tool card so the room sees the peek.
253
262
  if (toolName === 'mcp__thinkpool__read_terminal') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkpool-pair",
3
- "version": "0.7.103",
3
+ "version": "0.7.105",
4
4
  "description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
5
5
  "type": "module",
6
6
  "bin": {