/** * Mustache `{{var}}` variable substitution engine for CANT agents and * `.cantbook` playbooks. * * This is the canonical SDK implementation of the {@link VariableResolver} * contract declared in `@cleocode/contracts`. The engine resolves double-brace * placeholders (e.g. `{{tech_stack}}`, `{{testing.framework}}`) at spawn time * from a multi-tier context (bindings → session → project-context.json → * environment → default → missing). * * Key guarantees (per R2-VARIABLE-SYNTAX-DESIGN.md §4): * * - Regex: `/\{\{\s*([a-zA-Z_][a-zA-Z0-9_.]*)\s*\}\}/g`. * - Dot-notation path access on `projectContext` (`{{context.foo.bar}}`). * - Recursion prevention — resolved values are NEVER re-scanned for * placeholders, so `{{a}} → "{{b}}"` remains literal. * - Strict vs lenient mode: strict fails the whole substitution on any * missing; lenient leaves the placeholder and reports it in `missing`. * - Case-sensitive matching. * - Scalar coercion (numbers, booleans → String); objects → JSON.stringify. * * Integration: `packages/core/src/orchestrate/spawn-ops.ts` (T1570: orchestrate-engine.ts deleted) * calls {@link substituteCantAgentBody} from within `orchestrateSpawnExecute` * so CANT agent bodies are resolved before the spawn prompt is assembled. * * @module agents/variable-substitution * @task T1238 Variable substitution engine + contracts types * @task T1232 CLEO Agents Architecture Remediation for v2026.4.110 */ import type { ResolvedVariable, SubstitutionContext, SubstitutionOptions, SubstitutionResult, VariableResolver } from '@cleocode/contracts'; /** * Canonical implementation of the {@link VariableResolver} contract. * * Stateless and reusable across call sites — instantiate once and share, or * use {@link defaultResolver} for the module-level singleton. * * @example * ```ts * const resolver = new DefaultVariableResolver(); * const result = resolver.resolve( * 'Tech stack: {{tech_stack}}', * { bindings: { tech_stack: 'TypeScript' } }, * ); * // result.text === 'Tech stack: TypeScript' * ``` * * @task T1238 */ export declare class DefaultVariableResolver implements VariableResolver { /** * Regex used to detect template variables. Exposed as a read-only class * field so subclasses and tests can assert against the canonical pattern. */ readonly pattern: RegExp; /** * Resolve and substitute `{{var}}` placeholders in `text`. * * The algorithm makes a single pass over the template — each match is * resolved against the tier chain, and the result list is used to drive a * second pass that performs the actual string replacement. This avoids * accidental recursion when a resolved value itself contains `{{…}}` * (the regex is applied to the ORIGINAL text, never the replacement). * * @param text - Template containing `{{var}}` placeholders. * @param context - Multi-tier resolver context. * @param options - Optional behaviour tweaks. * @returns A {@link SubstitutionResult} envelope. */ resolve(text: string, context: SubstitutionContext, options?: SubstitutionOptions): SubstitutionResult; /** * Extract the unique variable names referenced by `text`. * * Used by template validators and bundle compilers that want to report * undeclared variables before runtime resolution runs. * * @param text - Template to scan. * @returns Deduplicated variable names in discovery order. */ extractVariables(text: string): string[]; /** * Validate that every variable in `requiredVars` resolves against `context`. * * Convenience wrapper around {@link resolveVariable} for callers that want * a pre-flight missing-var report without substituting anything. * * @param requiredVars - Variables the caller considers mandatory. * @param context - Resolver context to validate against. * @returns `{ valid: true, missing: [] }` when every variable resolves, * else `{ valid: false, missing: [...] }`. */ validate(requiredVars: readonly string[], context: SubstitutionContext): { valid: boolean; missing: string[]; }; /** * Resolve a single variable against the tier chain. * * Precedence (highest → lowest): * * 1. `bindings` (flat key lookup) * 2. `sessionContext` (flat key lookup) * 3. `projectContext` (dot-notation via {@link resolveNested}) * 4. `env` — tries each configured prefix (default: `CLEO_`, `CANT_`); * the variable name is upper-cased and dots are replaced with * underscores (`{{foo.bar}}` → `CLEO_FOO_BAR`). * * @param varName - Variable name as captured by the pattern. * @param context - Resolver context. * @param options - Options (only `envPrefixes` is consulted here). * @returns The resolved tier or `null` when every tier missed. */ private resolveVariable; } /** * Shared module-level resolver instance. Use for one-off call sites that do * not need custom configuration. Instantiating {@link DefaultVariableResolver} * directly is also cheap — no persistent state is kept. * * @task T1238 */ export declare const defaultResolver: VariableResolver; /** * Result envelope returned by {@link loadProjectContext}. */ export interface LoadProjectContextResult { /** Parsed JSON contents, or `null` when the file is missing or invalid. */ context: Record | null; /** Absolute path that was checked (for diagnostics). */ path: string; /** `true` when the file was found, read, and parsed successfully. */ loaded: boolean; /** Human-readable reason when `loaded === false`. */ reason?: string; } /** * Load `.cleo/project-context.json` from the project root. * * Best-effort: missing / malformed files return `loaded: false` with a * diagnostic reason rather than throwing. Callers (spawn engine) MUST * continue gracefully — project context is an optional tier of the * substitution resolver chain. * * NEVER hard-codes `.cleo` — delegates to {@link getCleoDirAbsolute} so the * `CLEO_DIR` env override and worktree scope are respected. * * @param projectRoot - Absolute path to the project root. * @returns {@link LoadProjectContextResult} with parsed JSON (or `null`). * @task T1238 */ export declare function loadProjectContext(projectRoot: string): LoadProjectContextResult; /** * Result envelope returned by {@link substituteCantAgentBody}. */ export interface SubstituteCantAgentBodyResult { /** Resolved CANT agent body (or the original body when no template vars were found). */ text: string; /** Variables resolved during substitution. */ resolved: ResolvedVariable[]; /** Variables referenced but unresolved (lenient mode). */ missing: string[]; /** `true` when substitution completed without strict-mode failure. */ success: boolean; /** Human-readable error when `success === false`. */ error?: string; /** `true` when {@link loadProjectContext} returned a parsed context. */ projectContextLoaded: boolean; } /** * Apply variable substitution to a CANT agent body (or any template string) * using the canonical spawn-time context stack. * * This is the integration entry point consumed by the orchestrate engine. It: * * 1. Loads `.cleo/project-context.json` via {@link loadProjectContext}. * 2. Builds a {@link SubstitutionContext} from `projectContext` + the caller's * `sessionContext`, `bindings`, and `env`. * 3. Delegates to {@link defaultResolver} in lenient mode (`strict: false`) * so partial substitutions do not block spawn. * * The returned envelope surfaces `missing` so the orchestrator can audit * which variables fell through — useful for detecting template drift as * project-context.json evolves. * * @param body - Template text (typically the CANT agent body). * @param params - Spawn-time context supplement. * @returns {@link SubstituteCantAgentBodyResult} with resolved text + diagnostics. * @task T1238 */ export declare function substituteCantAgentBody(body: string, params: { projectRoot: string; sessionContext?: Record; bindings?: Record; env?: Record; options?: SubstitutionOptions; }): SubstituteCantAgentBodyResult; //# sourceMappingURL=variable-substitution.d.ts.map