/** * Canonical spawn payload composer — the single public API for subagent * payload construction. * * Layering (ADR-069 D002): spawn lives in the Workflow layer. Workflow MAY * use Conduit (Messaging) to dispatch inter-agent DMs; Conduit MUST NOT * know about the harness or the orchestrator (ADR-069 D002). All physical * reads/writes to `tasks.db`, `brain.db`, `conduit.db`, and `signaldock.db` * MUST flow through the umbrella DataAccessor / `openCleoDb()` chokepoint * (Storage layer; T9050). * * @see ADR-069 (CLEO Coordination Layers) * * Wraps the T882 {@link buildSpawnPrompt} engine with the coherence * invariants required by the T889 epic: * * - {@link resolveAgent} registry lookup (W2-4) → populates the tier-ranked * `ResolvedAgent` envelope on every payload. * - {@link resolveHarnessHint} dedup decision (W3-2) → skips the ~9KB * tier-1 CLEO-INJECTION embed when the harness already loads it. * - Auto-tier selection by role (T892 — simple mapping here; richer * role/size/type heuristics land in W3-x). * - {@link checkAtomicity} worker file-scope gate (W3-3) → detects * worker spawns that lack explicit AC.files. * * Callers of the legacy {@link buildSpawnPrompt} continue to work unchanged. * New call sites should reach for {@link composeSpawnPayload} instead: it is * the only public entry point that emits the full `SpawnPayload` envelope * with traceability metadata, atomicity verdict, and dedup accounting. * * @module orchestration/spawn * @task T889 Orchestration Coherence v3 * @task T890 Tier auto-select (placeholder here, full impl in W3-x) * @task T891 Canonical composer (W3-1) * @task T893 Harness-aware dedup (W3-2) */ import type { DatabaseSync } from 'node:sqlite'; import type { AgentSpawnCapability, AgentTier, ResolvedAgent, Task } from '@cleocode/contracts'; import { type AtomicityResult } from './atomicity.js'; import { type HarnessHint } from './harness-hint.js'; import { type ConduitSubscriptionConfig, type SpawnProtocolPhase, type SpawnTier } from './spawn-prompt.js'; import { type ThinAgentEnforcementMode } from './thin-agent.js'; /** * Options accepted by {@link composeSpawnPayload}. * * Every field is optional. The composer applies the following defaults: * * - `tier` → derived from role: orchestrator=2, lead=1, worker=0 * - `role` → derived from `ResolvedAgent.orchLevel` (0/1/2 → o/l/w) * - `harnessHint` → {@link resolveHarnessHint} cascade result * - `agentId` → `'cleo-subagent'` until the classify() router ships * - `embedInjection` → inferred from `harnessHint` (claude-code → false) * - `skipAtomicityCheck` → false (always gate workers) * * @task T889 / W3-1 */ export interface ComposeSpawnPayloadOptions { /** Force a specific tier. Default: auto-select from role. */ tier?: SpawnTier; /** * Force role. Default: resolved from `ResolvedAgent.orchLevel`. * Mirrors the `AgentSpawnCapability` taxonomy so the atomicity guard can * consume the role directly without remapping. */ role?: AgentSpawnCapability; /** Harness hint override. Default: {@link resolveHarnessHint}. */ harnessHint?: HarnessHint; /** Project root for path + profile resolution. Default: `process.cwd()`. */ projectRoot?: string; /** * Explicit agent id. Default: `'cleo-subagent'` (classify router not yet * wired — T891 will swap this default for the classify() result). */ agentId?: string; /** * Force CLEO-INJECTION embed even when the harness has it loaded. * * Default: embed IFF `harnessHint !== 'claude-code'`. Override to `true` * for audited/offline captures where the full prompt must be archived * verbatim regardless of the running harness. */ embedInjection?: boolean; /** * Skip the atomicity gate. Reserved for orchestrator-invoked meta-spawns * (e.g. the `cleo orchestrate` engine itself spawning lead coordinators) * that are allowed to exceed the worker file budget. * * Default: `false`. When `true`, the composer returns * `atomicity.allowed = true` with no further checks. */ skipAtomicityCheck?: boolean; /** * Atomicity scope declaration for the child worker. * * When set to `'orchestrator-defer'`, the composer passes the waiver flag * into {@link checkAtomicity} so that a worker without declared files is * granted the spawn rather than rejected with `E_ATOMICITY_NO_SCOPE`. The * returned {@link SpawnPayload.atomicity} will carry * `atomicity_waiver: 'orchestrator-scope-tier1-call'` — callers MUST * persist this in the manifest entry. * * This field is intentionally distinct from {@link skipAtomicityCheck}: * `skipAtomicityCheck` is a silent hard bypass; `scope` is an auditable * structured waiver that travels through the normal `checkAtomicity` path. * * MUST NOT be used by tier-0 (direct user / CLI) callers. * * @task T9214 */ scope?: 'orchestrator-defer'; /** * Protocol phase to dispatch. Default: {@link autoDispatch} result for the * task. Callers that have already dispatched externally can pin the * phase here to avoid a redundant classification. */ protocol?: SpawnProtocolPhase | string; /** * Orchestrator's active session id to thread into the prompt so the * subagent logs every mutation against the caller's session. */ sessionId?: string | null; /** * CANT peer identifier for the spawned agent (T1260 PSYCHE E3). * * Used as the `peerId` parameter when calling `buildRetrievalBundle` to * fetch peer-scoped memory. Defaults to `'global'` when not provided. * * Callers that know the target agent's CANT peer id should supply it here * for accurate peer memory scoping. * * @task T1260 PSYCHE E3 */ peerId?: string; /** * Resolved tool allowlist for the agent. When present, the composer runs * {@link enforceThinAgent} against it as a dispatch-time defense-in-depth * check. Workers carrying spawn-capable tools (`Agent`, `Task`) are * rejected (strict mode) or stripped (strip mode) per * {@link thinAgentEnforcement}. * * Leave `undefined` when the caller has not resolved a flat tool list yet * — the guard becomes a no-op and composition proceeds normally. * * @task T931 Thin-agent runtime enforcer */ tools?: readonly string[]; /** * Thin-agent enforcement mode. Defaults to `'strict'` — any worker payload * that still contains `Agent` or `Task` is rejected with * {@link ThinAgentViolationError}. * * - `'strict'` (default) — throw on violation. * - `'strip'` — silently remove the offending tools, surface * the strip in `payload.meta.thinAgent`. * - `'off'` — escape hatch. Reserved for audited owner * overrides; emits `payload.meta.thinAgent. * bypassed = true`. * * Only consulted when {@link tools} is provided. * * @task T931 Thin-agent runtime enforcer */ thinAgentEnforcement?: ThinAgentEnforcementMode; /** * Absolute path to the pre-provisioned worktree for this task. * * When provided, the spawn prompt includes a `## Worktree Setup (REQUIRED)` * section that names the path and enforces the context-isolation boundary. * Passed through verbatim to {@link buildSpawnPrompt}. * * @task T1140 — worktree-by-default spawn prompt */ worktreePath?: string; /** * Git branch name for the worktree (e.g. `task/T1234`). * * Only meaningful when {@link worktreePath} is set. * Defaults to `task/` inside {@link buildSpawnPrompt}. * * @task T1140 */ worktreeBranch?: string; /** * CONDUIT A2A subscription configuration. * * When provided, a `## CONDUIT Subscription` section is injected into * tier-1 and tier-2 prompts. Omitted for tier-0 prompts and when not set. * Passed through verbatim to {@link buildSpawnPrompt}. * * @task T1252 CONDUIT A2A * @task T1253 wire orchestrate-engine */ conduitSubscription?: ConduitSubscriptionConfig; /** * Glob patterns excluded from the worktree via spawn-clone-exclude filter * (T9226). Passed through verbatim to {@link buildSpawnPrompt}. * * @task T9226 */ spawnCloneExclude?: readonly string[]; /** * Per-step skill allowlist from a cantbook `agentic` node's `allowed_skills` * (T1947 · M4 cantbook done-gate). * * When present, the composer intersects the resolved agent's skill baseline * (Tier-0/1/2 — see {@link ResolvedAgent.skills}) with this list and surfaces * the result on {@link SpawnPayload.effectiveSkills}. The allowlist can only * RESTRICT the baseline, never expand it — a skill named here that the agent * does not already carry is NOT added. When `undefined` the baseline passes * through unchanged (additive). * * @task T1947 */ allowedSkills?: readonly string[]; /** * Per-step tool allowlist from a cantbook `agentic` node's `allowed_tools` * (T1947 · M4 cantbook done-gate). * * When present, the composer intersects the spawn's tool baseline (the * {@link tools} list, AFTER the thin-agent guard has run) with this list and * surfaces the result on {@link SpawnPayload.effectiveTools}. Restrict-only, * never expand — identical semantics to {@link allowedSkills}. * * @task T1947 */ allowedTools?: readonly string[]; } /** * Full payload returned by {@link composeSpawnPayload}. * * Contains both the ready-to-use prompt string AND the traceability * metadata the orchestrator needs for its manifest and telemetry. Callers * that only want the prompt should read `payload.prompt`; everything else * is diagnostic. */ export interface SpawnPayload { /** Task id the spawn is about (mirrors `task.id`). */ taskId: string; /** Agent id the spawn is routed to. */ agentId: string; /** Role the agent will execute as. */ role: AgentSpawnCapability; /** Tier of the rendered prompt. */ tier: SpawnTier; /** Harness hint that drove the dedup decision. */ harnessHint: HarnessHint; /** Full resolved agent envelope (see {@link ResolvedAgent}). */ resolvedAgent: ResolvedAgent; /** Atomicity verdict from the worker file-scope guard. */ atomicity: AtomicityResult; /** * The fully-resolved spawn prompt. Copy-pastable into any LLM runtime * (Claude, GPT-4, Gemini) that accepts a system-prompt string. */ prompt: string; /** * Memory retrieval bundle assembled by `buildRetrievalBundle` (T1260 PSYCHE E3). * * Present when `options.sessionId` was provided and `buildRetrievalBundle` * succeeded. May have empty arrays until T1147 W7 sweep (.132) promotes * legacy entries from `'unswept-pre-T1151'` to `'swept-clean'`. * Callers MUST NOT crash when this is `undefined` or when arrays are empty. * * Registered as the M4 injection primitive — reusable by hooks, CANT, CONDUIT, * and Sentient proposer via the canonical import path. * * @task T1260 PSYCHE E3 */ retrievalBundle?: import('@cleocode/contracts').RetrievalBundle; /** * The agent's EFFECTIVE skill set for this spawn (T1947 · M4). * * Equals {@link ResolvedAgent.skills} (the Tier-0/1/2 baseline) when the node * declared no `allowed_skills`; otherwise the INTERSECTION of that baseline * with {@link ComposeSpawnPayloadOptions.allowedSkills} (restrict-only). The * downstream skill executor loads exactly these skills — enforcement, not * prose. Always present so consumers never branch on `undefined`. * * @task T1947 */ effectiveSkills: readonly string[]; /** * The spawn's EFFECTIVE tool set for this spawn (T1947 · M4), or `undefined` * when the caller supplied no tool baseline (`options.tools`). * * When a tool baseline was supplied, equals it (post thin-agent guard) when * the node declared no `allowed_tools`; otherwise the INTERSECTION of the * baseline with {@link ComposeSpawnPayloadOptions.allowedTools} (restrict-only). * * @task T1947 */ effectiveTools?: readonly string[]; /** Traceability / accounting metadata. */ meta: SpawnPayloadMeta; } /** * Persona classification metadata attached to {@link SpawnPayloadMeta}. * * Sourced from {@link classifyTask}. Callers can surface this in spawn * manifests and telemetry to audit the classify→route decision. * * @task T891 CANT persona wiring */ export interface SpawnPayloadPersonaMeta { /** Resolved agent id chosen by the classifier. */ readonly agentId: string; /** Spawn role the classifier assigned. */ readonly role: 'orchestrator' | 'lead' | 'worker'; /** Confidence score in [0, 1]. */ readonly confidence: number; /** Human-readable explanation of the routing decision. */ readonly reason: string; /** * `true` when the classifier demoted the result to `cleo-subagent` because * no rule cleared the {@link CLASSIFY_CONFIDENCE_FLOOR}. Callers SHOULD * surface the accompanying `warning` in telemetry. */ readonly usedFallback: boolean; /** Warning message emitted when `usedFallback === true`. */ readonly warning?: string; } /** * Diagnostic metadata attached to every {@link SpawnPayload}. */ export interface SpawnPayloadMeta { /** Tier the resolved agent was sourced from (mirrors `resolvedAgent.tier`). */ sourceTier: AgentTier; /** Characters saved by skipping the tier-1 CLEO-INJECTION embed. */ dedupSavedChars: number; /** Character length of the final prompt. */ promptChars: number; /** Protocol phase the prompt was rendered for. */ protocol: string; /** ISO 8601 timestamp when the payload was generated. */ generatedAt: string; /** Pinned composer contract version — bump on breaking changes. */ composerVersion: '3.0.0'; /** * Persona classification summary from {@link classifyTask}. Always present * — even when the caller passed an explicit `agentId` (in which case * `classify.agentId` matches the override and `classify.confidence = 1.0`). * * @task T891 CANT persona wiring */ classify: SpawnPayloadPersonaMeta; /** * Thin-agent enforcement summary. Present only when the composer was given * a `tools` allowlist and therefore executed {@link enforceThinAgent}. * * @task T931 Thin-agent runtime enforcer */ thinAgent?: SpawnPayloadThinAgentMeta; /** * Per-step skill/tool allowlist enforcement summary (T1947). Present only * when the node declared `allowed_skills` and/or `allowed_tools` (i.e. the * composer was given {@link ComposeSpawnPayloadOptions.allowedSkills} / * `allowedTools`). * * @task T1947 */ allowlist?: SpawnPayloadAllowlistMeta; } /** * Summary of the T931 thin-agent guard run for a given spawn payload. Attached * to {@link SpawnPayloadMeta.thinAgent} when the composer invoked * {@link enforceThinAgent}. * * @task T931 Thin-agent runtime enforcer */ export interface SpawnPayloadThinAgentMeta { /** Enforcement mode that ran. */ readonly mode: ThinAgentEnforcementMode; /** * Tools removed from the effective allowlist. Populated in `'strip'` mode * when a worker had `Agent`/`Task` tools removed; empty otherwise. */ readonly stripped: readonly string[]; /** * `true` when the guard was skipped because `mode === 'off'`. Surfaces to * audit logs so escape-hatch usage is visible. */ readonly bypassed: boolean; } /** * Summary of the T1947 per-step allowlist intersection for a given spawn. * Attached to {@link SpawnPayloadMeta.allowlist} when the node declared an * `allowed_skills` and/or `allowed_tools` list. * * @task T1947 */ export interface SpawnPayloadAllowlistMeta { /** * Skill slugs dropped because they were in the baseline but absent from the * node's `allowed_skills`. Empty when no skill allowlist was declared. */ readonly removedSkills: readonly string[]; /** * Tool names dropped because they were in the baseline but absent from the * node's `allowed_tools`. Empty when no tool allowlist was declared (or no * tool baseline was supplied). */ readonly removedTools: readonly string[]; /** * `true` when a declared skill allowlist intersected the baseline down to the * empty set — surfaced so callers can warn. The spawn still proceeds with no * skills (matches the T1947 AC2 "empty set → warning + proceed" contract). */ readonly skillsEmptiedByAllowlist: boolean; /** * `true` when a declared tool allowlist intersected the tool baseline down to * the empty set. */ readonly toolsEmptiedByAllowlist: boolean; } /** * Compose a fully-populated spawn payload for a given task. * * Resolution pipeline: * * 1. Resolve the agent via {@link resolveAgent} (4-tier precedence). * 2. Determine role (explicit option > derived from `orchLevel`). * 3. Resolve tier (explicit option > role-default). * 4. Resolve harness hint via {@link resolveHarnessHint} cascade. * 5. Run the {@link checkAtomicity} guard (unless explicitly skipped). * 6. Render the prompt via the T882 {@link buildSpawnPrompt} engine. * 7. Package everything in a {@link SpawnPayload} with traceability meta. * * Atomicity violations are surfaced in `payload.atomicity.allowed` rather * than thrown — callers choose how to react. Throwing the * {@link AtomicityViolationError} is still available by inspecting the * `atomicity.code` field and raising at the CLI boundary. * * @param db - Open handle to the global `signaldock.db`. Caller owns * lifecycle; the composer does not close it. * @param task - Task record being dispatched. * @param options - See {@link ComposeSpawnPayloadOptions}. * @returns A {@link SpawnPayload} with the prompt, atomicity verdict, and * traceability metadata. * * @task T889 / T891 / W3-1 */ export declare function composeSpawnPayload(db: DatabaseSync, task: Task, options?: ComposeSpawnPayloadOptions): Promise; //# sourceMappingURL=spawn.d.ts.map