CTL2 is a small TypeScript codec for deterministic, reversible task handovers. It turns a validated structured task into a compact canonical string and decodes that string without asking a model to infer missing information.
It is intended for systems that need to preview, compare, store, sign, or transmit the exact task that an agent or worker will receive.
Try every format and API in the interactive CTL2 playground. It runs entirely in your browser; task content is never uploaded.
Use CTL2 when you need:
- one explicit task contract shared by producers and consumers;
- byte-for-byte deterministic serialization;
- a compact, human-reviewable transport representation;
- strict rejection of unknown, reordered, truncated, or malformed fields;
- fixed decoder limits for untrusted input;
- backwards-compatible decoding of existing CTL1 records.
Do not use CTL2 as encryption, authentication, authorization, secret scanning, digital signing, or a general-purpose object serializer. CTL2 serializes one deliberately narrow task schema.
- Lossless:
decode(encode(task))returns the complete validated task. - Deterministic: equal tasks and options produce byte-for-byte equal payloads.
- Canonical: field order, token spelling, escaping, and dictionary-code order are fixed.
- Strict: unknown versions, fields, tokens, references, and malformed escapes fail closed.
- Bounded: payload bytes, decoded bytes, dictionaries, references, and lists have fixed limits.
- Unicode-safe: text and byte counts use Unicode-aware matching and UTF-8 measurement.
- Portable: browser-compatible ESM plus Node-compatible ESM and CommonJS builds are included.
- Compatible: stored canonical CTL1 payloads remain decodable; new payloads are CTL2.
CTL2 is compact serialization, not a privacy boundary. Anyone who can read a payload can decode its content.
Install directly from the immutable public GitHub release tag without registry authentication:
npm install github:lcherone/ctl2#v0.3.0Or install from GitHub Packages after authenticating npm for npm.pkg.github.com:
npm config set @lcherone:registry https://npm.pkg.github.com
npm install @lcherone/[email protected]The runtime supports Node.js 18 and later. The ESM build also works in modern browsers that provide TextEncoder.
This is the exact input used by examples/basic.mjs:
import { decode, encode, type HandoverTask } from '@lcherone/ctl2';
const task: HandoverTask = {
version: 1,
id: 'demo-1',
title: 'Fix parser',
objective: 'Reject invalid input.',
operation: 'fix',
targets: ['src/parser.ts'],
requirements: ['Preserve API'],
acceptanceCriteria: ['Tests pass'],
constraints: [],
verification: ['npm test'],
assumptions: [],
exclusions: [],
contextClassification: 'public'
};
const payload = encode(task);
const decoded = decode(payload);encode(task) returns this exact CTL2 payload:
C2|d:|i:demo-1|t:Fix+parser|o:f|g:Reject+invalid+input.|x:src/parser.ts|r:Preserve+API|a:Tests+pass|c:|v:npm+test|u:|e:|k:p
decode(payload) returns:
{
"version": 1,
"id": "demo-1",
"title": "Fix parser",
"objective": "Reject invalid input.",
"operation": "fix",
"targets": ["src/parser.ts"],
"requirements": ["Preserve API"],
"acceptanceCriteria": ["Tests pass"],
"constraints": [],
"verification": ["npm test"],
"assumptions": [],
"exclusions": [],
"contextClassification": "public"
}The checked-in example is executed in the test suite, including the exact serialized output.
Use validate() at user-input or network boundaries:
import { validate } from '@lcherone/ctl2';
const result = validate(incomingPayload);
if (result.valid) {
console.log(result.task.objective);
} else {
console.error(result.errors[0]);
}ValidationResult is a discriminated union, so TypeScript makes task available only after result.valid is true.
Compact mode is the default:
const payload = encode(task);
const samePayload = encode(task, { dictionaryMode: 'compact' });It assigns dictionary codes only to repeated words whose entry plus all references makes the complete payload smaller.
Abstract mode assigns deterministically shuffled codes to repeated words, up to the fixed dictionary limit:
const abstractPayload = encode(task, { dictionaryMode: 'abstract' });The same task always receives the same abstract mapping. A semantic task change can reshuffle it. Abstract mode may be larger than compact mode and is not encryption, obfuscation, or a privacy feature.
Both modes use the same C2 wire grammar and decode() function.
| Export | Purpose |
|---|---|
encode(task, options?) |
Validate and serialize a task as canonical CTL2 |
decode(payload) |
Decode canonical CTL2 or legacy CTL1 and validate the result |
validate(payload) |
Decode without throwing and return a discriminated result |
compareFormats(task) |
Produce compact CTL2, abstract CTL2, JSON, and UTF-8 byte counts |
decodeTransferPayload(format, payload) |
Decode ctl, ctl-abstract, or strict JSON input |
handoverTaskSchema |
Strict Zod runtime schema for a complete task |
operationSchema |
Runtime schema for task operations |
classificationSchema |
Runtime schema for context classifications |
transferFormatSchema |
Runtime schema for supported transfer formats |
CTL_LIMITS |
Read-only codec resource limits |
All public functions, options, result fields, schemas, and task fields include generated JSDoc. See API.md for signatures, errors, and focused examples.
| Field | Type | Meaning |
|---|---|---|
version |
1 |
Structured task schema version |
id |
string | Stable identifier, 1–160 characters |
title |
string | Human-readable title, 1–240 characters |
objective |
string | Complete required outcome |
operation |
enum | implement, fix, refactor, review, or test |
targets |
string[] | Explicit files, modules, repositories, URLs, or systems in scope |
requirements |
string[] | Required behaviour and implementation requirements |
acceptanceCriteria |
string[] | Observable completion conditions |
constraints |
string[] | Compatibility, safety, and change-scope boundaries |
verification |
string[] | Commands or human checks expected to verify the outcome |
assumptions |
string[] | Facts relied upon but not yet verified |
exclusions |
string[] | Work explicitly outside the task |
contextClassification |
enum | public, internal-approved, or restricted |
The object schema is strict: missing required fields, empty strings, unsupported enum values, and extra object properties are rejected. Every list must be present even when empty.
C2|d:dictionary|i:text|t:text|o:operation|g:text|x:list|r:list|a:list|c:list|v:list|u:list|e:list|k:classification
Spaces encode as +, path separators stay readable as /, list items are comma-separated, and empty lists use an empty field. Reserved or ambiguous characters are percent-encoded. A reference such as ~0 expands to the matching dictionary entry; a literal ~ is always percent-encoded.
Canonical field order is part of the protocol. A decoder rejects missing, extra, duplicated, unknown, or reordered fields. See WIRE_FORMAT.md for the complete grammar and decoding rules.
CTL_LIMITS defines:
| Limit | Value |
|---|---|
| Encoded payload | 1,000,000 UTF-8 bytes |
| Aggregate decoded task text | 1,000,000 UTF-8 bytes |
| Dictionary entries | 4,096 |
| Items in each task list | 4,096 |
| Dictionary references | 65,536 |
The encoder applies the same limits as the decoder and never returns a payload that its own decoder would reject for dictionary size or reference count.
ES modules:
import { decode, encode } from '@lcherone/ctl2';CommonJS:
const { decode, encode } = require('@lcherone/ctl2');Browser bundlers should consume the ESM export. The runtime does not use Node's Buffer, filesystem, process, network, or native modules.
Before accepting CTL2 in a production boundary:
- Validate and authorize the canonical task before encoding it.
- Apply secret scanning and data-classification rules to task fields, not compact tokens.
- Pin a released version or immutable Git tag.
- Catch
decode()errors or usevalidate()for untrusted payloads. - Keep the default resource limits unless a future version explicitly makes them configurable.
- Add transport integrity or signatures when payload tampering matters.
- Store the declared format and package version beside long-lived payloads.
- Test a representative production payload against a new release before upgrading.
See ADOPTION.md for boundary patterns, migration guidance, observability, and rollout advice.
- API reference and examples
- Wire-format specification
- Architecture and invariants
- Production adoption guide
- Testing strategy
- Security policy and threat boundary
- Contribution guide
- Release history
| Path | Responsibility |
|---|---|
src/schema.ts |
Public task contract and runtime Zod schemas |
src/codec.ts |
Canonical encoding, strict decoding, limits, and format comparison |
src/index.ts |
Single supported public export surface |
examples/ |
Executable input/output and validation examples |
test/ |
Canonical fixtures, compatibility, malformed input, limits, fuzzing, and package smoke tests |
docs/ |
Protocol, API, architecture, adoption, and testing documentation |
.github/workflows/ |
CI, dependency review, static analysis, and release publication |
The published runtime supports Node.js 18 or later. Building from source uses Node.js 22.22.2 or later because the documentation and lint toolchain intentionally tracks current supported releases.
npm ci
npm run check
npm run build
npm pack --dry-runnpm run check runs source and Markdown linting, strict TypeScript checks, API-documentation validation, tests with coverage thresholds, dual-format builds, example tests, package smoke tests, and package-manifest validation.
See TESTING.md for individual commands and CI coverage.
CTL2 follows semantic versioning. Changes to canonical CTL2 encoding, accepted wire grammar, exported task shape, decoder limits, or public API require an explicit compatibility decision. Decoders may continue accepting older formats after encoders move forward.