thinkpool-pair 0.7.241 → 0.7.243
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 +32 -11
- package/cross-terminal.mjs +53 -2
- package/hermes-session.mjs +10 -3
- package/package.json +1 -1
- package/thinkpool-room-prompt.mjs +6 -0
package/bridge.mjs
CHANGED
|
@@ -105,7 +105,7 @@ const flowRedispatch = new Map()
|
|
|
105
105
|
// wave BEFORE overrun. Lives bridge-side because waves dispatch across separate
|
|
106
106
|
// broadcasts; without persistent state the cap can never bite.
|
|
107
107
|
const flowBudgets = new Map()
|
|
108
|
-
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, laneStatusOf } from './cross-terminal.mjs'
|
|
108
|
+
import { formatPeek, PEEK, siblingsOf, resolveSibling, crossPostDecision, CROSSPOST, spawnDecision, SPAWN, CROSSROOM, formatPairRoster, crossRoomPostDecision, formatRoomNow, formatClosableHint, closeLaneDecision, laneStatusOf, nativeClaudeFallbackHint } from './cross-terminal.mjs'
|
|
109
109
|
import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce, sendInterruptedContinue, shouldAttemptNativeResume, supersedeInterruptedResume } from './interrupted-resume.mjs'
|
|
110
110
|
import { turnInFlight } from './update-gate.mjs'
|
|
111
111
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage } from './session-store.mjs'
|
|
@@ -114,7 +114,7 @@ import { SIDE_HANDOFF_PROMPT, SIDE_ROLE_PROMPT, appendSideContext, assistantText
|
|
|
114
114
|
import { planMeterLine } from './plan-meters.mjs'
|
|
115
115
|
import { makeThrottledTrack } from './presence.mjs'
|
|
116
116
|
import { resolveAnonKey, DEFAULT_SUPABASE_URL } from './supabase-key.mjs'
|
|
117
|
-
import { buildTerminalRolePrompt } from './thinkpool-room-prompt.mjs'
|
|
117
|
+
import { buildTerminalRolePrompt, HERMES_VISIBLE_WORKER_FALLBACK_RULE } from './thinkpool-room-prompt.mjs'
|
|
118
118
|
|
|
119
119
|
// Public client creds (the same anon values the web app ships — safe to embed).
|
|
120
120
|
// Override with TP_SUPABASE_URL / TP_SUPABASE_ANON if you ever need to.
|
|
@@ -2014,10 +2014,19 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2014
2014
|
const resolveAgentOpen = (args, { worker = false } = {}) => {
|
|
2015
2015
|
const runtime = args?.runtime || entry.runtime || 'claude'
|
|
2016
2016
|
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.' }
|
|
2017
|
-
|
|
2017
|
+
if (runtime === 'hermes' && args?.provider) return { error: 'A Hermes terminal uses its isolated Hermes profile and ACP model catalog; bridge provider IDs do not apply. Omit provider and choose an exact Hermes model ID.' }
|
|
2018
|
+
if (runtime === 'hermes' && (!worker || entry.runtime !== 'hermes')) return { error: 'A Hermes worker can only be opened by a top-level Hermes terminal with a live ACP model catalog.' }
|
|
2019
|
+
const catalog = runtime === 'codex' ? readCodexModels() : runtime === 'hermes' ? (entry.models || []) : []
|
|
2018
2020
|
if (runtime === 'codex' && args?.model && !modelCatalogValues(catalog).has(args.model)) {
|
|
2019
2021
|
return { error: `Could not open a Codex terminal on ${JSON.stringify(args.model)} — that model is not in this host's visible Codex catalog.` }
|
|
2020
2022
|
}
|
|
2023
|
+
if (runtime === 'hermes' && !args?.model) return { error: 'Opening a Hermes worker requires an explicit exact model ID from this session\'s ACP catalog.' }
|
|
2024
|
+
if (runtime === 'hermes' && !modelCatalogValues(catalog).has(args.model)) {
|
|
2025
|
+
return { error: `Could not open a Hermes worker on ${JSON.stringify(args.model)} — that exact model is not in this parent session's ACP catalog.` }
|
|
2026
|
+
}
|
|
2027
|
+
if (runtime === 'hermes' && args?.mode && !['default', 'acceptEdits'].includes(args.mode)) {
|
|
2028
|
+
return { error: `Hermes ACP does not structurally support ${JSON.stringify(args.mode)} mode. Use default or acceptEdits; Flow/plan/review safety remains disabled.` }
|
|
2029
|
+
}
|
|
2021
2030
|
if (runtime === 'claude' && !args?.provider && args?.model && /^gpt-/i.test(args.model)) {
|
|
2022
2031
|
return { error: `Could not open a Claude terminal on Codex model ${JSON.stringify(args.model)}. Choose runtime="codex" or a Claude model.` }
|
|
2023
2032
|
}
|
|
@@ -2070,7 +2079,11 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2070
2079
|
if (entry.peekCount > PEEK.perTurnCap) {
|
|
2071
2080
|
return { content: [{ type: 'text', text: `Cross-terminal read limit reached for this turn (${PEEK.perTurnCap}). Continue with what you have, or ask the people in the room.` }] }
|
|
2072
2081
|
}
|
|
2082
|
+
const sib = siblingsOf({ selfId: id, sessions, terms, names: termNames })
|
|
2083
|
+
const target = args?.terminal ? resolveSibling(sib, args.terminal) : null
|
|
2084
|
+
const targetEntry = target?.kind === 'agent' ? sessions.get(target.id) : null
|
|
2073
2085
|
const text = formatPeek({ selfId: id, sessions, terms, names: termNames, terminal: args?.terminal, lines: args?.lines })
|
|
2086
|
+
+ nativeClaudeFallbackHint({ parentRuntime: entry.runtime, parentModels: entry.models, targetRuntime: targetEntry?.runtime, targetLog: targetEntry?.log })
|
|
2074
2087
|
return { content: [{ type: 'text', text }] }
|
|
2075
2088
|
},
|
|
2076
2089
|
),
|
|
@@ -2252,13 +2265,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2252
2265
|
// not receive this tool, so the hierarchy is capability-enforced.
|
|
2253
2266
|
...(canSpawnWorkers ? [tool(
|
|
2254
2267
|
'spawn_terminal',
|
|
2255
|
-
'Open a WORKER SUB-TERMINAL (Claude or
|
|
2268
|
+
'Open a visible WORKER SUB-TERMINAL (Claude, Codex, or—only from a Hermes parent—Hermes) 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. A Hermes worker requires an explicit exact model ID from the parent ACP catalog. After a native Claude account/provider failure, do not retry Claude aliases on that provider; follow read_terminal\'s one-time visible Hermes fallback guidance. A Hermes review worker in default mode is permission-gated, not structurally read-only—deny/stop writes and never claim Flow safety. 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.',
|
|
2256
2269
|
{
|
|
2257
2270
|
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"'),
|
|
2258
2271
|
task: z.string().optional().describe('an initial task to hand the new lane immediately; omit to open it idle'),
|
|
2259
|
-
model: z.string().optional().describe('
|
|
2272
|
+
model: z.string().optional().describe('model for the chosen runtime; Hermes workers require an exact ACP catalog ID such as nous:anthropic/claude-sonnet-4.6'),
|
|
2260
2273
|
sliceType: z.enum(['scaffold', 'feature', 'fix', 'review']).optional().describe('optional cascade slice tier; omitted preserves normal inheritance/default, explicit model wins'),
|
|
2261
|
-
runtime: z.enum(['claude', 'codex']).optional().describe('agent runtime for the new lane;
|
|
2274
|
+
runtime: z.enum(['claude', 'codex', 'hermes']).optional().describe('agent runtime for the new lane; Hermes is allowed only from a top-level Hermes parent with an explicit catalog model'),
|
|
2262
2275
|
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'),
|
|
2263
2276
|
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.'),
|
|
2264
2277
|
},
|
|
@@ -2283,7 +2296,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2283
2296
|
ref: String(sid).slice(0, 8),
|
|
2284
2297
|
name: termNames[sid] || undefined,
|
|
2285
2298
|
spawnedBy: s.spawnedBy,
|
|
2286
|
-
busy: s.session?.turnActive ?? false,
|
|
2299
|
+
busy: (s.session?.turnActive ?? false) || (s.pending?.size ?? 0) > 0,
|
|
2287
2300
|
}))))
|
|
2288
2301
|
// Record this spawn, pruning timestamps that have already left the window
|
|
2289
2302
|
// (identical predicate to recentSpawnCount) so the array can't grow unbounded.
|
|
@@ -2363,7 +2376,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2363
2376
|
// the web's code-close), which no-ops if the id isn't a live session.
|
|
2364
2377
|
...(!entry.flowRole ? [tool(
|
|
2365
2378
|
'close_terminal',
|
|
2366
|
-
'Close an agent lane that YOU opened with spawn_terminal (identified by ref/id/name). You can only close lanes you spawned yourself — never a sibling someone else is working in, and never the main terminal.
|
|
2379
|
+
'Close an IDLE agent lane that YOU opened with spawn_terminal (identified by ref/id/name). Active turns and permission waits are protected: collect the completed result with read_terminal before closing. You can only close lanes you spawned yourself — never a sibling someone else is working in, and never the main terminal.',
|
|
2367
2380
|
{
|
|
2368
2381
|
terminal: z.string().describe('ref, id, or name of the lane you spawned (from read_terminal)'),
|
|
2369
2382
|
},
|
|
@@ -2374,7 +2387,15 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2374
2387
|
if (!target) return okText(`No terminal matches "${args?.terminal}". Call read_terminal with no argument to list the open terminals.`)
|
|
2375
2388
|
const te = sessions.get(target.id)
|
|
2376
2389
|
if (!te) return okText(`Terminal ${target.ref} is not an agent lane (or already closed) — only spawned agent lanes can be closed this way.`)
|
|
2377
|
-
|
|
2390
|
+
const close = closeLaneDecision({
|
|
2391
|
+
callerId: id,
|
|
2392
|
+
spawnedBy: te.spawnedBy,
|
|
2393
|
+
busy: (te.session?.turnActive ?? false) || (te.pending?.size ?? 0) > 0,
|
|
2394
|
+
})
|
|
2395
|
+
if (!close.ok && close.reason === 'not-owner') return okText(`Terminal ${target.ref} was not opened by you — you can only close lanes you spawned yourself. Leave it to the person running it.`)
|
|
2396
|
+
if (!close.ok && close.reason === 'busy') {
|
|
2397
|
+
return okText(`Terminal ${target.ref} is still working or waiting for a permission decision. close_terminal will not kill an active turn. Wait until read_terminal reports it idle/completed, collect its result, then close it. If it is genuinely stuck, ask a person to press Stop.`)
|
|
2398
|
+
}
|
|
2378
2399
|
endStructured(target.id)
|
|
2379
2400
|
return okText(`Closed agent lane ${target.ref}${target.name ? ` ("${target.name}")` : ''}.`)
|
|
2380
2401
|
},
|
|
@@ -2446,13 +2467,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2446
2467
|
// on the init `system` event (see onEvent) — a sendTurn before the input stream is consumed
|
|
2447
2468
|
// is silently lost (the 2026-07-02 auto-resume bug). Empty string → nothing to carry.
|
|
2448
2469
|
entry.pendingRecap = (typeof carryRecap === 'string' && carryRecap.trim()) ? carryRecap : null
|
|
2449
|
-
const terminalRolePrompt = buildTerminalRolePrompt({
|
|
2470
|
+
const terminalRolePrompt = [buildTerminalRolePrompt({
|
|
2450
2471
|
spawnedBy: entry.spawnedBy,
|
|
2451
2472
|
spawnDepth: entry.spawnDepth,
|
|
2452
2473
|
cascadeRole: entry.cascadeRole,
|
|
2453
2474
|
flowRole: entry.flowRole,
|
|
2454
2475
|
sideParent: entry.sideParent,
|
|
2455
|
-
})
|
|
2476
|
+
}), runtime === 'hermes' && canSpawnWorkers ? HERMES_VISIBLE_WORKER_FALLBACK_RULE : ''].filter(Boolean).join('\n\n')
|
|
2456
2477
|
entry.session = startStructuredSession(runtime, {
|
|
2457
2478
|
// laneModel, NOT the raw `model` param: the SDK's `model` option OVERRIDES the
|
|
2458
2479
|
// ANTHROPIC_MODEL supplied by resolveProviderEnv() in `env` below, so an inherited
|
package/cross-terminal.mjs
CHANGED
|
@@ -220,7 +220,48 @@ export const formatPeek = ({ selfId, sessions, terms, names = {}, terminal, line
|
|
|
220
220
|
} else {
|
|
221
221
|
body = cleanPtyTail(hit.buf, n).join('\n') || '(no output yet)'
|
|
222
222
|
}
|
|
223
|
-
return `Terminal ${label(hit)} (${hit.kind} · ${hit.cmd}) — last ${n} lines:\n${body}`
|
|
223
|
+
return `Terminal ${label(hit)} (${hit.kind} · ${hit.cmd}) · ${laneStatusText(hit)} — last ${n} lines:\n${body}`
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// A native Claude Code account failure is provider-wide, not model-specific. Retrying
|
|
227
|
+
// Sonnet → Opus → Haiku on the same built-in Anthropic path only repeats the same
|
|
228
|
+
// failure and burns orchestration time. Keep this classifier deliberately narrow:
|
|
229
|
+
// transient model/rate/overload errors must not disable the whole provider.
|
|
230
|
+
export const nativeClaudeProviderAccessFailed = (events) => {
|
|
231
|
+
let text = ''
|
|
232
|
+
try { text = typeof events === 'string' ? events : JSON.stringify(events || '') }
|
|
233
|
+
catch { text = String(events || '') }
|
|
234
|
+
return /organization has disabled (?:claude )?subscription access|(?:claude )?subscription access (?:has been |is )?disabled|invalid anthropic (?:api )?key|invalid x-api-key|not logged in to claude|(?:run|use) \/login[^\n]{0,80}claude|no valid anthropic (?:api )?key/i.test(text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const HERMES_CLAUDE_REVIEW_PREFERENCE = Object.freeze([
|
|
238
|
+
'nous:anthropic/claude-sonnet-4.6',
|
|
239
|
+
'nous:anthropic/claude-sonnet-4.5',
|
|
240
|
+
'nous:anthropic/claude-sonnet-4',
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
export const preferredHermesClaudeReviewModel = (models = []) => {
|
|
244
|
+
const values = new Set((Array.isArray(models) ? models : []).map((model) => (
|
|
245
|
+
typeof model === 'string' ? model : model?.value || model?.slug
|
|
246
|
+
)).filter(Boolean))
|
|
247
|
+
for (const model of HERMES_CLAUDE_REVIEW_PREFERENCE) if (values.has(model)) return model
|
|
248
|
+
for (const model of values) if (/^nous:anthropic\/claude-sonnet-/i.test(model)) return model
|
|
249
|
+
for (const model of values) if (/^nous:anthropic\/claude-haiku-/i.test(model)) return model
|
|
250
|
+
for (const model of values) if (/^nous:anthropic\/claude-opus-/i.test(model)) return model
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Returned alongside read_terminal output so the orchestrator gets a structural
|
|
255
|
+
// provider verdict at the exact moment it observes the failed worker. A Hermes
|
|
256
|
+
// fallback is a visible ordinary worker, never hidden delegate_task and never Flow.
|
|
257
|
+
// Hermes ACP 0.18.2 cannot enforce read-only review, so the note states the honest
|
|
258
|
+
// permission-gated boundary instead of pretending mode="default" is structural plan.
|
|
259
|
+
export const nativeClaudeFallbackHint = ({ parentRuntime, parentModels, targetRuntime, targetLog } = {}) => {
|
|
260
|
+
if (targetRuntime !== 'claude' || !nativeClaudeProviderAccessFailed(targetLog)) return ''
|
|
261
|
+
const stop = 'ThinkPool provider status: native Claude access failed at the account/provider level. Do not retry Sonnet, Opus, Haiku, or another model alias on the same native Claude provider during this task.'
|
|
262
|
+
const fallback = parentRuntime === 'hermes' ? preferredHermesClaudeReviewModel(parentModels) : null
|
|
263
|
+
if (!fallback) return `\n\n${stop} Use a different available runtime/provider once, or report the blocker.`
|
|
264
|
+
return `\n\n${stop} For a visible manual review fallback, spawn exactly one worker with runtime="hermes", model=${JSON.stringify(fallback)}, mode="default", and sliceType="review". This Hermes lane is permission-gated, not structurally read-only: instruct it not to edit, deny/stop any write request, and do not treat its review as a Flow safety proof. Never use Hermes delegate_task.`
|
|
224
265
|
}
|
|
225
266
|
|
|
226
267
|
// ── Live per-turn room snapshot ("ROOM NOW") ─────────────────────────────────
|
|
@@ -376,8 +417,18 @@ export const spawnDecision = ({ hop = 0, spawnTimes = [], now = 0, spawnedLive =
|
|
|
376
417
|
// Only lanes this caller spawned are suggested: close_terminal refuses the others, and
|
|
377
418
|
// suggesting a sibling's lane is how you get an agent killing someone else's work.
|
|
378
419
|
export const CLOSABLE_HINT_MAX = 6
|
|
420
|
+
export const closeLaneDecision = ({ callerId, spawnedBy, busy } = {}) => {
|
|
421
|
+
if (!callerId || spawnedBy !== callerId) return { ok: false, reason: 'not-owner' }
|
|
422
|
+
if (busy) return { ok: false, reason: 'busy' }
|
|
423
|
+
return { ok: true }
|
|
424
|
+
}
|
|
425
|
+
|
|
379
426
|
export const closableLanes = (callerId, lanes = []) =>
|
|
380
|
-
(Array.isArray(lanes) ? lanes : []).filter((l) => l &&
|
|
427
|
+
(Array.isArray(lanes) ? lanes : []).filter((l) => l && closeLaneDecision({
|
|
428
|
+
callerId,
|
|
429
|
+
spawnedBy: l.spawnedBy,
|
|
430
|
+
busy: l.busy,
|
|
431
|
+
}).ok)
|
|
381
432
|
|
|
382
433
|
export const formatClosableHint = (callerId, lanes = [], max = CLOSABLE_HINT_MAX) => {
|
|
383
434
|
const idle = closableLanes(callerId, lanes)
|
package/hermes-session.mjs
CHANGED
|
@@ -193,11 +193,18 @@ export function startHermesSession({
|
|
|
193
193
|
}
|
|
194
194
|
const serverModel = state?.models?.currentModelId || null
|
|
195
195
|
activeModel = requestedModel || serverModel || activeModel
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
196
|
+
// A spawned Hermes worker may be pinned to an exact model from its parent's
|
|
197
|
+
// ACP catalog. Acknowledge that model BEFORE publishing the initial system/
|
|
198
|
+
// models events; otherwise the worker runs the requested model while the UI
|
|
199
|
+
// and persisted resume record lie that it still uses the profile default.
|
|
200
|
+
if (requestedModel && requestedModel !== serverModel) {
|
|
199
201
|
await client.request('session/set_model', { sessionId, modelId: activeModel })
|
|
200
202
|
}
|
|
203
|
+
const publishedModels = state?.models
|
|
204
|
+
? { ...state.models, currentModelId: activeModel }
|
|
205
|
+
: activeModel ? { currentModelId: activeModel, availableModels: [] } : state?.models
|
|
206
|
+
mapper.startSession({ sessionId, models: publishedModels, modes: state?.modes, commands: [] })
|
|
207
|
+
started = true
|
|
201
208
|
const acpMode = activeMode === 'acceptEdits' ? 'accept_edits' : 'default'
|
|
202
209
|
if (state?.modes?.availableModes?.some((item) => item.id === acpMode) && state.modes.currentModeId !== acpMode) {
|
|
203
210
|
await client.request('session/set_mode', { sessionId, modeId: acpMode })
|
package/package.json
CHANGED
|
@@ -57,3 +57,9 @@ export const CODEX_THINKPOOL_FIRST_TURN_PREAMBLE = [
|
|
|
57
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.',
|
|
58
58
|
...THINKPOOL_REMOTE_DELIVERY_RULES,
|
|
59
59
|
].join(' ')
|
|
60
|
+
|
|
61
|
+
// Hermes mains can dispatch visible ordinary workers, including a Claude-family
|
|
62
|
+
// model served through the parent's Nous Portal catalog when native Claude Code
|
|
63
|
+
// account access is unavailable. This is manual orchestration only: ACP 0.18.2
|
|
64
|
+
// does not expose structural read-only review policy, and Flow remains disabled.
|
|
65
|
+
export const HERMES_VISIBLE_WORKER_FALLBACK_RULE = 'HERMES VISIBLE-WORKER FALLBACK: spawn_terminal may open a visible Hermes worker only from a top-level Hermes terminal and only with an explicit exact model ID advertised in this session\'s ACP catalog. If a native Claude Code worker reports an account/provider authentication or subscription-access failure, treat the entire native Claude provider as unavailable for the rest of the task—do not retry Sonnet, Opus, or Haiku aliases on it. For a manual Claude-family review fallback, use runtime="hermes", the exact Nous Claude model suggested by read_terminal, mode="default", and sliceType="review". This fallback is permission-gated, not structurally read-only: instruct it not to edit, stop/deny any write request, and never call it Flow/reviewer safety. Never use hidden Hermes delegate_task.'
|