thinkpool-pair 0.7.232 → 0.7.233
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 +103 -26
- package/claude-session.mjs +11 -11
- package/codex-session.mjs +1 -1
- package/cross-terminal.mjs +6 -8
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +9 -6
package/bridge.mjs
CHANGED
|
@@ -1962,6 +1962,49 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
1962
1962
|
// 1.5s saveSession debounce window) and on shutdown (so events since the last
|
|
1963
1963
|
// debounced write aren't lost). Contract #2: restart resumes, no lost messages.
|
|
1964
1964
|
entry.flush = () => flushSession(room, id, sessionData())
|
|
1965
|
+
// Main-terminal creation and Ensemble dispatch are different capabilities.
|
|
1966
|
+
// A main terminal (human-opened or agent-opened via open_main_terminal) may
|
|
1967
|
+
// dispatch workers. A spawned/Side/Flow lane can never elevate itself or a
|
|
1968
|
+
// child into a conductor merely by wording its task that way.
|
|
1969
|
+
const isIndependentMain = !entry.spawnedBy && !entry.sideParent && !entry.flowRole && (entry.spawnDepth || 0) === 0
|
|
1970
|
+
const canOpenMainConductor = isIndependentMain && entry.cascadeRole !== 'conductor'
|
|
1971
|
+
const canSpawnWorkers = isIndependentMain
|
|
1972
|
+
const resolveAgentOpen = (args, { worker = false } = {}) => {
|
|
1973
|
+
const runtime = args?.runtime || entry.runtime || 'claude'
|
|
1974
|
+
if (runtime === 'codex' && args?.provider) return { error: 'A Codex terminal uses its Codex/OpenAI login in v1; custom bridge providers are not wired to Codex yet. Omit provider or open a Claude terminal for that provider.' }
|
|
1975
|
+
const catalog = runtime === 'codex' ? readCodexModels() : []
|
|
1976
|
+
if (runtime === 'codex' && args?.model && !modelCatalogValues(catalog).has(args.model)) {
|
|
1977
|
+
return { error: `Could not open a Codex terminal on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.` }
|
|
1978
|
+
}
|
|
1979
|
+
if (runtime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
1980
|
+
return { error: `Could not open a Claude terminal on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.` }
|
|
1981
|
+
}
|
|
1982
|
+
return {
|
|
1983
|
+
runtime,
|
|
1984
|
+
model: args?.model || (worker && args?.sliceType
|
|
1985
|
+
? spawnedLaneModelFor({ sliceType: args.sliceType, runtime, catalog })
|
|
1986
|
+
: undefined),
|
|
1987
|
+
mode: args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode),
|
|
1988
|
+
provider: args?.provider,
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
const consumeTerminalOpenBudget = () => {
|
|
1992
|
+
const now = Date.now()
|
|
1993
|
+
const gate = spawnDecision({
|
|
1994
|
+
hop: 0,
|
|
1995
|
+
spawnTimes: entry.spawnTimes || [],
|
|
1996
|
+
now,
|
|
1997
|
+
// Main terminals do not consume the Ensemble plan ceiling. The pure
|
|
1998
|
+
// gate still supplies the shared kill-switch, machine cap, and burst cap.
|
|
1999
|
+
spawnedLive: 0,
|
|
2000
|
+
totalLive: sessions.size + terms.size,
|
|
2001
|
+
plan: ownerPlan,
|
|
2002
|
+
disabled: process.env.TP_SPAWN_OFF === '1',
|
|
2003
|
+
})
|
|
2004
|
+
if (!gate.ok) return gate
|
|
2005
|
+
entry.spawnTimes = [...(entry.spawnTimes || []).filter((t) => Number.isFinite(t) && t > now - SPAWN.windowMs), now]
|
|
2006
|
+
return gate
|
|
2007
|
+
}
|
|
1965
2008
|
// B2 cross-terminal peek — an in-process, READ-ONLY tool the agent can call to
|
|
1966
2009
|
// see a SIBLING terminal's recent output (vs B1's always-on @pool digest). The
|
|
1967
2010
|
// handler closes over the module-scope `sessions`/`terms` Maps + this session's
|
|
@@ -2106,43 +2149,81 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2106
2149
|
return okText(`Delivered to terminal ${target.ref} (${target.cmd}). It will respond in its own lane; check back with read_terminal.`)
|
|
2107
2150
|
},
|
|
2108
2151
|
)] : []),
|
|
2109
|
-
//
|
|
2152
|
+
// MAIN CONDUCTOR — create the same independent structured terminal the web
|
|
2153
|
+
// "+ New terminal" path creates: no spawnedBy marker, no Ensemble ownership,
|
|
2154
|
+
// depth 0. Only an ordinary independent main terminal receives this tool;
|
|
2155
|
+
// conductor mains, spawned workers, Side lanes, and Flow lanes cannot use it.
|
|
2156
|
+
...(canOpenMainConductor ? [tool(
|
|
2157
|
+
'open_main_terminal',
|
|
2158
|
+
'Open a separate independent MAIN terminal to conduct a Cascade. Use this whenever a person explicitly asks to open, launch, start, or spawn a new Cascade/conductor terminal. This is the top-level room lifecycle, NOT Ensemble dispatch: the new terminal has no spawnedBy owner and appears as a main terminal. Never substitute spawn_terminal, which creates worker sub-terminals only. Give the conductor its full initial task; it will use spawn_terminal for its workers.',
|
|
2159
|
+
{
|
|
2160
|
+
name: z.string().max(80).optional().describe('short label for the main conductor, e.g. "Cascade · Multi-device chaos"'),
|
|
2161
|
+
task: z.string().min(1).describe('complete initial Cascade brief for the conductor'),
|
|
2162
|
+
model: z.string().optional().describe('optional conductor model, e.g. opus / gpt-5.6-sol'),
|
|
2163
|
+
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime; defaults to inheriting this main terminal'),
|
|
2164
|
+
provider: z.string().optional().describe('optional registered LLM provider id for a Claude conductor'),
|
|
2165
|
+
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode; defaults to inheriting this main terminal, except plan falls back to default'),
|
|
2166
|
+
},
|
|
2167
|
+
async (args) => {
|
|
2168
|
+
const okText = (t) => ({ content: [{ type: 'text', text: t }] })
|
|
2169
|
+
const resolved = resolveAgentOpen(args)
|
|
2170
|
+
if (resolved.error) return okText(resolved.error)
|
|
2171
|
+
const gate = consumeTerminalOpenBudget()
|
|
2172
|
+
if (!gate.ok) return okText(gate.reason)
|
|
2173
|
+
const newId = randomUUID()
|
|
2174
|
+
const newRef = String(newId).slice(0, 8)
|
|
2175
|
+
const fromRef = String(id).slice(0, 8)
|
|
2176
|
+
if (args?.name) { termNames[newId] = String(args.name).slice(0, 80); saveNames(room, termNames) }
|
|
2177
|
+
openStructured({
|
|
2178
|
+
id: newId,
|
|
2179
|
+
runtime: resolved.runtime,
|
|
2180
|
+
model: resolved.model,
|
|
2181
|
+
provider: resolved.provider,
|
|
2182
|
+
mode: resolved.mode,
|
|
2183
|
+
spawnDepth: 0,
|
|
2184
|
+
cascadeRole: 'conductor',
|
|
2185
|
+
hop: 0,
|
|
2186
|
+
})
|
|
2187
|
+
const conductor = sessions.get(newId)
|
|
2188
|
+
if (!conductor) {
|
|
2189
|
+
if (args?.name) { delete termNames[newId]; saveNames(room, termNames) }
|
|
2190
|
+
return okText('Could not open the main conductor terminal — the machine terminal cap may have just been reached.')
|
|
2191
|
+
}
|
|
2192
|
+
conductor.peekCount = 0; conductor.postCount = 0; conductor.spawnTimes = []
|
|
2193
|
+
announce()
|
|
2194
|
+
const msg = `[Task from main terminal ${fromRef}'s agent — opened as an independent MAIN CASCADE CONDUCTOR terminal, not an Ensemble child]\n${String(args.task).trim()}`
|
|
2195
|
+
const evt = { kind: 'you', text: msg, by: `main terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2196
|
+
stampEvent(evt); pushLog(conductor, evt); bcast('code-event', { term: newId, evt })
|
|
2197
|
+
try { conductor.session.sendTurn(msg) }
|
|
2198
|
+
catch { return okText(`Opened main conductor ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but it may still be starting — the initial task could not be delivered.`) }
|
|
2199
|
+
return okText(`Opened independent main Cascade conductor ${newRef}${args?.name ? ` ("${args.name}")` : ''} and handed it the task. It is a main terminal, not an Ensemble lane.`)
|
|
2200
|
+
},
|
|
2201
|
+
)] : []),
|
|
2202
|
+
// Tier C+ — SPAWN a fresh WORKER lane the caller OWNS. The motivating bug:
|
|
2110
2203
|
// an agent fanned a research task into SIBLINGS that were already busy (a
|
|
2111
2204
|
// release lane, a Q&A lane) because it had no way to make its own lanes.
|
|
2112
2205
|
// This opens a brand-new structured session server-side (the same path the
|
|
2113
2206
|
// web's "+ New terminal" and the restore loop use), so announce() surfaces
|
|
2114
2207
|
// its tab in the open web UI live. Auto-allowed by the PreToolUse gate but
|
|
2115
|
-
// bounded by spawnDecision (
|
|
2116
|
-
// cap, TP_SPAWN_OFF kill-switch).
|
|
2117
|
-
//
|
|
2118
|
-
|
|
2119
|
-
...(!entry.flowRole ? [tool(
|
|
2208
|
+
// bounded by spawnDecision (one-worker-tier hop bound, burst window, the live terminal
|
|
2209
|
+
// cap, TP_SPAWN_OFF kill-switch). Spawned lanes are always workers and do
|
|
2210
|
+
// not receive this tool, so the hierarchy is capability-enforced.
|
|
2211
|
+
...(canSpawnWorkers ? [tool(
|
|
2120
2212
|
'spawn_terminal',
|
|
2121
|
-
'Open a
|
|
2213
|
+
'Open a WORKER SUB-TERMINAL (Claude or Codex) in the ThinkPool Ensemble. spawn_terminal is worker-only: it can never create a Cascade conductor or main terminal. When a person asks for a separate Cascade/conductor terminal, use open_main_terminal instead. The runtime defaults to inheriting this main/conductor terminal, or pass `runtime`. Choose `model` explicitly when practical: Codex Luna/Mini for mechanical work, Terra for ordinary feature/fix work, and a balanced tier for adversarial review; never select Sol for routine workers. Pass sliceType=scaffold for mechanical work, feature/fix for builders, and review for adversarial verification. Give the worker a bounded initial task, collect its result with read_terminal, then ALWAYS close_terminal it.',
|
|
2122
2214
|
{
|
|
2123
2215
|
name: z.string().max(80).optional().describe('a short label for the new lane so the room (and you) can find it, e.g. "Research: topologies"'),
|
|
2124
2216
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
2125
2217
|
model: z.string().optional().describe('optional model for the chosen runtime, e.g. opus / sonnet / gpt-5.6-sol'),
|
|
2126
2218
|
sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
|
|
2127
|
-
cascadeRole: z.enum(['conductor', 'worker']).optional().describe('structural Cascade identity injected into the child system preamble: conductor may dispatch one worker tier; worker does the assigned slice directly'),
|
|
2128
2219
|
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime for the new lane; defaults to inheriting this lane'),
|
|
2129
2220
|
provider: z.string().optional().describe('optional registered LLM provider id to run this lane on (from the account\'s provider registry); omit for the default Claude/Anthropic path'),
|
|
2130
2221
|
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan']).optional().describe('permission mode for the new lane; defaults to inheriting YOUR current mode. Raising a lane to bypassPermissions from a non-bypass lane asks the room to confirm once.'),
|
|
2131
2222
|
},
|
|
2132
2223
|
async (args) => {
|
|
2133
2224
|
const okText = (t) => ({ content: [{ type: 'text', text: t }] })
|
|
2134
|
-
const
|
|
2135
|
-
if (
|
|
2136
|
-
const childCatalog = childRuntime === 'codex' ? readCodexModels() : []
|
|
2137
|
-
if (childRuntime === 'codex' && args?.model && !modelCatalogValues(childCatalog).has(args.model)) {
|
|
2138
|
-
return okText(`Could not open a Codex lane on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.`)
|
|
2139
|
-
}
|
|
2140
|
-
if (childRuntime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
2141
|
-
return okText(`Could not open a Claude lane on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.`)
|
|
2142
|
-
}
|
|
2143
|
-
const childModel = args?.model || (args?.sliceType
|
|
2144
|
-
? spawnedLaneModelFor({ sliceType: args.sliceType, runtime: childRuntime, catalog: childCatalog })
|
|
2145
|
-
: undefined)
|
|
2225
|
+
const resolved = resolveAgentOpen(args, { worker: true })
|
|
2226
|
+
if (resolved.error) return okText(resolved.error)
|
|
2146
2227
|
const now = Date.now()
|
|
2147
2228
|
const gate = spawnDecision({
|
|
2148
2229
|
hop: entry.hop || 0,
|
|
@@ -2176,13 +2257,12 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2176
2257
|
// PM-m1 — never INHERIT plan mode into a spawned worker lane: a lane spawned from a
|
|
2177
2258
|
// plan-mode lane would stall on ExitPlanMode cards. An explicit args.mode still wins;
|
|
2178
2259
|
// otherwise inherit, but fall plan → default (an autonomous worker doesn't plan-gate).
|
|
2179
|
-
const childMode = args?.mode || (entry.mode === 'plan' ? 'default' : entry.mode)
|
|
2180
2260
|
const childSpawnDepth = (entry.spawnDepth || 0) + 1
|
|
2181
2261
|
const childHop = (entry.hop || 0) + 1
|
|
2182
2262
|
// Stamp ownership/depth BEFORE the runtime starts so its first system
|
|
2183
2263
|
// preamble is truthful. Mutating ne.spawnedBy after openStructured was
|
|
2184
2264
|
// too late: Codex/Claude had already booted with the top-level wording.
|
|
2185
|
-
openStructured({ id: newId, runtime:
|
|
2265
|
+
openStructured({ id: newId, runtime: resolved.runtime, model: resolved.model, provider: resolved.provider, mode: resolved.mode, spawnedBy: id, spawnDepth: childSpawnDepth, cascadeRole: 'worker', hop: childHop })
|
|
2186
2266
|
const ne = sessions.get(newId)
|
|
2187
2267
|
if (!ne) { if (args?.name) { delete termNames[newId]; saveNames(room, termNames) } return okText('Could not open a new lane — the terminal cap may have just been reached. Close one and retry.') }
|
|
2188
2268
|
ne.peekCount = 0; ne.postCount = 0; ne.spawnTimes = []
|
|
@@ -2190,10 +2270,7 @@ function openStructured({ id, runtime = 'claude', model, effort, resume, log, co
|
|
|
2190
2270
|
if (args?.task) {
|
|
2191
2271
|
// The user-visible relay reinforces (but never defines) the structural
|
|
2192
2272
|
// role already injected by the system preamble above.
|
|
2193
|
-
const
|
|
2194
|
-
? 'you are its Cascade conductor terminal, responsible for worker sub-terminals'
|
|
2195
|
-
: 'you are its worker sub-terminal, not a top-level room terminal'
|
|
2196
|
-
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool cross-terminal; ${roleRelay}]\n${args.task}`
|
|
2273
|
+
const msg = `[Task from terminal ${fromRef}'s agent — relayed via ThinkPool Ensemble; you are its WORKER SUB-TERMINAL, never a main terminal or Cascade conductor]\n${args.task}`
|
|
2197
2274
|
const evt = { kind: 'you', text: msg, by: `terminal ${fromRef} (agent)`, crosspost: true, relaySourceName: termNames[id] || undefined }
|
|
2198
2275
|
stampEvent(evt); pushLog(ne, evt); bcast('code-event', { term: newId, evt })
|
|
2199
2276
|
try { ne.session.sendTurn(msg) } catch { return okText(`Opened lane ${newRef}${args?.name ? ` ("${args.name}")` : ''}, but it may still be starting — could not hand off the task. Try post_to_terminal shortly.`) }
|
package/claude-session.mjs
CHANGED
|
@@ -200,10 +200,10 @@ const TP_ROOM_REMINDER = [
|
|
|
200
200
|
THINKPOOL_DESIGN_DELIVERY_RULE,
|
|
201
201
|
'REMOTE USER: the people here may have no terminal and no access to this host — never ask them to run a local command, open a local file, or check something on the machine. Anything host-side, YOU run, and show the result.',
|
|
202
202
|
'SHOW, don\'t narrate: a picture of the result beats a wall of text on a phone — err toward more screenshots.',
|
|
203
|
-
'
|
|
203
|
+
'TERMINAL HIERARCHY: obey your authoritative TERMINAL ROLE. Conductors are independent main terminals; Ensemble lanes are workers only. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal — NEVER spawn_terminal. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Only conductor-capable roles fan worker slices out with spawn_terminal. Leaf, worker, Side, and managed Flow lanes work directly. Never use built-in invisible Task/Agent subagents or hijack a busy sibling.',
|
|
204
204
|
'PEER: before substantive work, check what the other lanes are doing (the ROOM NOW snapshot below, read_terminal for detail; list_sessions/read_session across rooms) — coordinate on shared files/branches instead of colliding.',
|
|
205
205
|
'WORKTREES: parallel lanes share one repo — before code edits run `git worktree list`; if linked worktrees exist, take your OWN worktree + branch, never the shared checkout or a branch another lane is on.',
|
|
206
|
-
'BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans
|
|
206
|
+
'BUILD WORKFLOW (default, no magic word): right-size within your TERMINAL ROLE — a trivial ask or delegated slice you just do; a conductor-capable role with a genuinely decomposable build FIRST writes a short plan in chat, THEN fans worker slices into visible spawn_terminal lanes and verifies them. Worker/leaf/Side/managed Flow roles do not fan out. A requested separate conductor uses open_main_terminal. Never plan-mode/ExitPlanMode; plans live in chat and lanes in the existing list.',
|
|
207
207
|
'Verify before claiming done — show runtime evidence you produced, not "should work, go test it".',
|
|
208
208
|
].join(' ')
|
|
209
209
|
|
|
@@ -516,25 +516,25 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
516
516
|
if (toolName === 'mcp__thinkpool__close_terminal') {
|
|
517
517
|
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: 'Auto-approved (ThinkPool cross-terminal close — only self-spawned lanes).' } }
|
|
518
518
|
}
|
|
519
|
-
// ThinkPool
|
|
520
|
-
//
|
|
519
|
+
// ThinkPool terminal creation — open_main_terminal creates an independent main
|
|
520
|
+
// conductor; spawn_terminal creates a worker the agent owns. Both inherit mode
|
|
521
521
|
// (`mode` here is THIS lane's current mode) unless an explicit `mode` overrides it.
|
|
522
522
|
// The only thing worth a human card is an ESCALATION: raising a child to
|
|
523
523
|
// bypassPermissions from a non-bypass lane (full unattended access the operator
|
|
524
524
|
// hasn't already opted into). If the spawner is already in bypass, children inherit
|
|
525
525
|
// bypass with NO prompt (you opted in once); a non-bypass child gates itself per
|
|
526
526
|
// action anyway, so no spawn prompt there either. Bridge-side spawnDecision still
|
|
527
|
-
// enforces the
|
|
528
|
-
if (toolName === 'mcp__thinkpool__spawn_terminal') {
|
|
527
|
+
// enforces the one-worker-tier bound / burst window / machineMax / TP_SPAWN_OFF regardless.
|
|
528
|
+
if (toolName === 'mcp__thinkpool__spawn_terminal' || toolName === 'mcp__thinkpool__open_main_terminal') {
|
|
529
529
|
const childMode = toolInput?.mode || mode // inherit this lane's mode by default
|
|
530
530
|
const escalating = childMode === 'bypassPermissions' && mode !== 'bypassPermissions'
|
|
531
531
|
if (!escalating) {
|
|
532
|
-
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: `Auto-approved (
|
|
532
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow', permissionDecisionReason: `Auto-approved (open a fresh ThinkPool terminal in ${childMode} mode — inherited, no escalation; bounded by room caps).` } }
|
|
533
533
|
}
|
|
534
534
|
let decision = 'deny'
|
|
535
535
|
try { decision = await requestPermission?.({ id: randomUUID(), toolName, input: { ...toolInput, mode: childMode }, risk: 'high' }) ?? 'deny' } catch { decision = 'deny' }
|
|
536
536
|
const allowed = decision === 'allow'
|
|
537
|
-
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: allowed ? 'allow' : 'deny', permissionDecisionReason: allowed ? 'Bypass
|
|
537
|
+
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: allowed ? 'allow' : 'deny', permissionDecisionReason: allowed ? 'Bypass terminal creation approved in the ThinkPool room — the new terminal runs autonomously.' : 'Bypass terminal creation denied in the room — do not retry as bypass; open it without mode:bypassPermissions or ask what to do.' } }
|
|
538
538
|
}
|
|
539
539
|
// ThinkPool cross-terminal POST (Tier C) WRITES into a sibling agent's lane.
|
|
540
540
|
// First a bridge-side precheck (kill-switch / loop-breaker / per-turn cap) so a
|
|
@@ -702,15 +702,15 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
702
702
|
// no summon word. The agent INFERS build intent and runs plan→(optional)fan-out→build→self-verify,
|
|
703
703
|
// right-sized to the task, surfaced in the room's EXISTING surfaces (chat + lane list) — never a new panel,
|
|
704
704
|
// never plan-mode's approval card. Trivial asks stay single-lane with zero ceremony.
|
|
705
|
-
'DEFAULT BUILD WORKFLOW (this IS your standard behaviour — no keyword and no "flow" word is ever required to trigger it; you infer it from the request itself). RIGHT-SIZE to the task within the authoritative TERMINAL ROLE, always: a TRIVIAL ask — a one-line fix, a small tweak, a question — you just do it directly, with no plan and no extra lanes (never tax small work with ceremony). A NON-TRIVIAL build that genuinely decomposes, when your role permits delegation — you FIRST write a short plan as a normal chat message (the approach, the concrete slices, and what you will verify), THEN fan
|
|
705
|
+
'DEFAULT BUILD WORKFLOW (this IS your standard behaviour — no keyword and no "flow" word is ever required to trigger it; you infer it from the request itself). RIGHT-SIZE to the task within the authoritative TERMINAL ROLE, always: a TRIVIAL ask — a one-line fix, a small tweak, a question — you just do it directly, with no plan and no extra lanes (never tax small work with ceremony). A NON-TRIVIAL build that genuinely decomposes, when your role permits delegation — you FIRST write a short plan as a normal chat message (the approach, the concrete slices, and what you will verify), THEN fan worker slices into visible Ensemble lanes with spawn_terminal, build, and self-verify each slice before calling it done. When the person explicitly asks you to launch a SEPARATE Cascade/conductor terminal, call open_main_terminal instead; never represent that request with spawn_terminal. Worker, leaf, Side, and managed Flow lanes complete their assigned work directly and do not fan out.',
|
|
706
706
|
'CRUCIAL RECONCILIATION for that workflow: it is NOT plan mode. Never call ExitPlanMode and never make the room wait behind a "plan ready — approve to start" card — your plan lives in the CHAT as a message, and your lanes live in the room\'s EXISTING terminal/lane list. Reuse only those two surfaces; there is no new Flow panel or mode to switch into, and you must not ask for one. Keep the plan and the lanes VISIBLE — that shared visibility is the whole point (it is the pair differentiator, and it catches bugs a single silent lane would hide); never collapse a decomposable build into one hidden lane just to look tidy.',
|
|
707
707
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
708
708
|
'CROSS-TERMINAL AWARENESS: this room may have other terminals open alongside yours — other agents working, or shells the people are driving. You have a READ-ONLY tool, read_terminal: call it with no arguments to list the other open terminals, or with a terminal ref/id/command to read that terminal\'s recent activity. Reach for it when your work depends on what another terminal is doing (e.g. someone says "see what the other terminal hit", or you need to coordinate with a sibling agent before acting). It only ever reads — it never changes another terminal. Identify a terminal by its NAME or its ref/id from the roster, never by an on-screen number like "Terminal 2" — those positional labels renumber when a terminal is closed, so they do not reliably point at a lane.',
|
|
709
709
|
'CROSS-TERMINAL HAND-OFF: you also have post_to_terminal(terminal, text) to send a message or task to ANOTHER AGENT terminal in this room (not a plain shell). Use it sparingly and only when the people clearly want the lanes to coordinate — e.g. "tell the backend terminal the API is ready", or to hand a sibling agent a concrete task. Every post requires a person in the room to approve a card before it is delivered, and an agent that was itself reached via a cross-post cannot post onward — so do not rely on it for chit-chat or loops. Prefer read_terminal to understand a sibling before you ever post to it.',
|
|
710
|
-
'
|
|
710
|
+
'TERMINAL CREATION CONTRACT: conductors and workers use different tools. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal(name?, task?, model?) — it creates an independent MAIN terminal with no Ensemble owner. Never use spawn_terminal for that request, and if open_main_terminal is unavailable say so instead of substituting. Use spawn_terminal(name?, task?, model?, sliceType?) only for bounded WORKER SUB-TERMINALS when your authoritative role permits delegation. Workers never receive the creation tools and never conduct. Do not dump work into busy siblings. Spawned workers inherit your permission mode by default; raising one to bypass asks the room once. Collect each worker with read_terminal and close_terminal immediately after using its result. Main conductors are independent terminals and are not owned/closed through Ensemble. Room caps and the kill-switch bind both creation paths.',
|
|
711
711
|
'CROSS-SESSION AWARENESS: the Ensemble reaches across your SESSIONS, not just the terminals in this room. list_sessions() lists your OTHER ThinkPool Code rooms — both your own rooms running on this machine AND your partner\'s rooms in the same pair, reachable over the per-pair bus (a room on the partner\'s machine shows its host). read_session(session, terminal?) reads recent activity inside one (omit `terminal` to list that room\'s terminals, or pass a ref/name to read that lane). Both are READ-ONLY — they never change another session, and they reach ONLY your own rooms and rooms you share with your partner, never a stranger\'s. Reach for them when work spans rooms — "what\'s the other project up to", "pick up where the other session left off", or to check a long-running task elsewhere before you act here.',
|
|
712
712
|
'CROSS-SESSION HAND-OFF: post_to_session(session, text, terminal?) sends a task or message to an agent in ANOTHER of your rooms — your own, or your partner\'s over the pair bus. Use it sparingly and only when the people clearly want the rooms to coordinate — e.g. hand the API room\'s agent a concrete follow-up once the frontend is ready. It is dual-consent: a person in YOUR room approves sending, and a person in the TARGET room approves receiving, before anything is delivered — so never rely on it for chit-chat or loops, and an agent that was itself reached via a cross-room post cannot post onward to a third room. It spends real model tokens in the other room (maybe on the other person\'s machine), so prefer read_session to understand a room before you ever post into it, and only post one concrete hand-off at a time. All of this works only under the ThinkPool account bridge; a standalone room sees just its own terminals.',
|
|
713
|
-
'SUBAGENT POLICY: in this room,
|
|
713
|
+
'SUBAGENT POLICY: in this room, a main conductor delegates worker slices through visible spawn_terminal Ensemble lanes. A requested separate conductor is created with open_main_terminal, never Ensemble. Worker, leaf, Side, and managed Flow lanes do their assigned work directly. Do NOT reach for built-in Task/Agent subagents: an in-process subagent is invisible to the room, cannot be peered at or steered, and its work is lost to the Ensemble.',
|
|
714
714
|
'RESEARCH LANE: you have a `research` tool that runs a REAL multi-source web search + adversarial verification and returns each claim marked HELD or REJECTED with citations. Reach for it when the people would genuinely benefit from looking something external up or settling a question of current fact — pricing, "is X still maintained / deprecated", "is that benchmark real", a debate over facts you are not sure of. Do NOT run it unprompted or for things you already know: first OFFER in plain language ("want me to spawn a research lane on that and check it?"), and only call `research(question)` once they agree — it spends real budget (plan-gated Free 5 / Plus 100 runs a month) and takes ~a minute. When it returns, present the held/rejected findings clearly and invite both people to weigh the sources, flagging any held claim that rests on a source they might not trust — that shared scrutiny is the point.',
|
|
715
715
|
'PEER FIRST: other lanes may be working in the same repo as you, right now. Before starting substantive work — and before any code edit that could overlap another lane — check what the room is doing: the ROOM NOW snapshot appended to your latest turn, or read_terminal for detail; list_sessions/read_session when the question spans your other rooms. If a sibling is touching the same files or branch, coordinate (read its lane, or raise it in chat) instead of colliding.',
|
|
716
716
|
'WORKTREES: parallel lanes share one machine and usually one repo. Run `git worktree list` before your first code edit; if linked worktrees exist, the shared main checkout is contended (and may be guard-blocked) — do your work in your OWN worktree on your OWN branch (`git worktree add <dir> -b <branch>`), and never edit a checkout or ride a branch another lane is using.',
|
package/codex-session.mjs
CHANGED
|
@@ -210,7 +210,7 @@ export function codexPublishAccess({ cwd, env = process.env, tmpdir = os.tmpdir(
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
export const CODEX_ROOM_CASCADE_REMINDER = [
|
|
213
|
-
'THINKPOOL ROOM WORKFLOW: obey the authoritative TERMINAL ROLE above.
|
|
213
|
+
'THINKPOOL ROOM WORKFLOW: obey the authoritative TERMINAL ROLE above. Conductors are independent main terminals; Ensemble lanes are workers only. When a person explicitly asks to open, launch, start, or spawn a separate Cascade/conductor terminal, use open_main_terminal — NEVER spawn_terminal. Use spawn_terminal only for worker slices from a role permitted to delegate. If open_main_terminal is unavailable, say so; never substitute an Ensemble child. Leaf, worker, Side, and managed Flow lanes work directly and must not delegate. Never use hidden in-process subagents. Each worker owns its own worktree. For a genuinely decomposable task in a conductor-capable role, state a short plan in chat, use sliceType scaffold for mechanical work, feature/fix for builders, and review for adversarial verification; review must use a balanced model tier. Read results, verify the integrated outcome, and close every worker you spawned.',
|
|
214
214
|
].join(' ')
|
|
215
215
|
|
|
216
216
|
export function buildCodexPrompt({ text, terminalRolePrompt, rolePrompt, roomContext, firstTurn = false }) {
|
package/cross-terminal.mjs
CHANGED
|
@@ -301,8 +301,9 @@ export const crossPostNeedsCard = ({ mode = 'default', spawnedByMe = false } = {
|
|
|
301
301
|
// OWN lanes. spawn_terminal gives it fresh lanes; this gate keeps that from
|
|
302
302
|
// becoming a fork bomb. Bounds, in order:
|
|
303
303
|
// • kill-switch (TP_SPAWN_OFF)
|
|
304
|
-
// • hop — a lane REACHED via cross-post/spawn (hop ≥ 1) cannot spawn onward
|
|
305
|
-
//
|
|
304
|
+
// • hop — a lane REACHED via cross-post/spawn (hop ≥ 1) cannot spawn onward.
|
|
305
|
+
// Cascade conductors now use the independent main-terminal lifecycle; every
|
|
306
|
+
// Ensemble-spawned lane is a worker (the loop breaker mirrors CROSSPOST).
|
|
306
307
|
// • machineMax — overall hard cap on LIVE lanes (sessions+terms): the runaway /
|
|
307
308
|
// don't-melt-the-laptop backstop, plan-INDEPENDENT.
|
|
308
309
|
// • dispatch ceiling — a SEPARATE, plan-tiered budget counting ONLY spawned lanes
|
|
@@ -323,12 +324,9 @@ export const crossPostNeedsCard = ({ mode = 'default', spawnedByMe = false } = {
|
|
|
323
324
|
// this stays a PURE function (no Date.now inside; the clock is injected, like
|
|
324
325
|
// spawnCount was before).
|
|
325
326
|
export const SPAWN = {
|
|
326
|
-
maxHop:
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// machineMax + the plan ceiling + the burst window + the kill-switch.
|
|
330
|
-
// Was 1 until 2026-07-10 (docs/specs/2026-07-10-cascade-spawn-depth.md):
|
|
331
|
-
// conductors dispatched by a person's lane could not dispatch at all.
|
|
327
|
+
maxHop: 1, // main terminal/conductor = hop 0 and may dispatch workers;
|
|
328
|
+
// Ensemble worker = hop 1 and can never spawn onward. Conductors
|
|
329
|
+
// are created via open_main_terminal, not spawn_terminal.
|
|
332
330
|
perWindowCap: 6, // spawn_terminal calls allowed per rolling window (burst breaker);
|
|
333
331
|
// 6 matches plusMaxSpawned so one wave can open a full crew (was 4)
|
|
334
332
|
windowMs: 10 * 60 * 1000, // sliding window over which perWindowCap applies (10 min)
|
package/package.json
CHANGED
|
@@ -34,23 +34,26 @@ export function buildTerminalRolePrompt({ spawnedBy = null, spawnDepth = 0, casc
|
|
|
34
34
|
if (sideParent) {
|
|
35
35
|
return `TERMINAL ROLE (authoritative): You are a SIDE SUB-TERMINAL attached to main terminal ${parentRef}. You are not an independent top-level terminal and not a Cascade conductor. Complete the bounded side task directly, do not fan out more terminals, and return a compact handoff to main.`
|
|
36
36
|
}
|
|
37
|
+
if (cascadeRole === 'conductor') {
|
|
38
|
+
if (!spawnedBy && depth === 0) {
|
|
39
|
+
return 'TERMINAL ROLE (authoritative): You are an independent MAIN CASCADE CONDUCTOR TERMINAL opened through the room\'s top-level terminal lifecycle. You are not an Ensemble child, spawned sub-terminal, Side lane, or Flow lane. Keep decomposition and integration here. Open only worker SUB-TERMINALS with spawn_terminal, collect and verify their results, and close them. Never call open_main_terminal yourself or delegate conductor responsibility to a worker.'
|
|
40
|
+
}
|
|
41
|
+
return `TERMINAL ROLE (authoritative): You are a MISCLASSIFIED ENSEMBLE SUB-TERMINAL at spawn depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. A spawned lane can never be a Cascade conductor. Work directly as a worker, do not spawn terminals, and report that the parent must use open_main_terminal for a separate conductor.`
|
|
42
|
+
}
|
|
37
43
|
if (!spawnedBy && depth === 0) {
|
|
38
|
-
return 'TERMINAL ROLE (authoritative): You are an independent TOP-LEVEL ThinkPool terminal opened directly in the room. You are not a spawned sub-terminal or worker lane. You may
|
|
44
|
+
return 'TERMINAL ROLE (authoritative): You are an independent TOP-LEVEL ThinkPool terminal opened directly in the room. You are not a spawned sub-terminal or worker lane. You may conduct the current task here and open worker SUB-TERMINALS with spawn_terminal. When a person explicitly asks you to open, launch, or start a separate Cascade/conductor terminal, use open_main_terminal — never spawn_terminal. Conductors are main terminals; Ensemble-spawned lanes are workers only.'
|
|
39
45
|
}
|
|
40
46
|
if (depth >= 2) {
|
|
41
47
|
return `TERMINAL ROLE (authoritative): You are a LEAF SUB-TERMINAL at spawn depth ${depth}, owned by parent terminal ${parentRef}. You are not top-level and not a conductor. Complete the assigned slice directly, do not spawn another terminal, and hand evidence/results back to your parent.`
|
|
42
48
|
}
|
|
43
|
-
if (cascadeRole === 'conductor') {
|
|
44
|
-
return `TERMINAL ROLE (authoritative): You are a CASCADE CONDUCTOR TERMINAL at spawn depth ${Math.max(1, depth)}, launched by parent terminal ${parentRef}. You are a visible conductor terminal, not one of the worker sub-terminals you will dispatch. Decompose the assigned Cascade, open bounded worker SUB-TERMINALS with spawn_terminal(cascadeRole="worker"), collect and verify their results, then close them. You are still owned by your parent and must return the integrated outcome there.`
|
|
45
|
-
}
|
|
46
49
|
if (cascadeRole === 'worker') {
|
|
47
50
|
return `TERMINAL ROLE (authoritative): You are a WORKER SUB-TERMINAL at spawn depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. You are not top-level and not a conductor. Complete the assigned slice directly, do not spawn another terminal, and hand evidence/results back to your parent.`
|
|
48
51
|
}
|
|
49
|
-
return `TERMINAL ROLE (authoritative): You are a SPAWNED SUB-TERMINAL at depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. You are not an independent top-level terminal
|
|
52
|
+
return `TERMINAL ROLE (authoritative): You are a SPAWNED WORKER SUB-TERMINAL at depth ${Math.max(1, depth)}, owned by parent terminal ${parentRef}. You are not an independent top-level terminal or a Cascade conductor. Complete the delegated task directly, do not spawn another terminal, and hand evidence/results back to your parent. If the task asks you to conduct, report that the parent must use open_main_terminal instead.`
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
|
|
53
56
|
'ENVIRONMENT (authoritative): You are Codex running inside a ThinkPool Code room, driven live through the thinkpool-pair bridge by a user and possibly a partner. This is a shared remote workspace, not an ordinary local terminal session; act accordingly.',
|
|
54
|
-
'MODEL TIERING FOR
|
|
57
|
+
'MODEL TIERING FOR TERMINALS: When calling open_main_terminal for a genuinely complex Cascade conductor, gpt-5.6-sol may be appropriate. When calling spawn_terminal for worker slices, explicitly choose gpt-5.6-luna or gpt-5.4-mini for mechanical/scaffold/search/formatting work, gpt-5.6-terra for ordinary feature/fix work, and a balanced tier for adversarial review. Never select Sol for routine workers. If a named model is unavailable, use the nearest advertised lower tier or omit model and allow runtime fallback.',
|
|
55
58
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
56
59
|
].join(' ')
|