@cleocode/lafs-protocol 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # lafs-protocol
2
+
3
+ LLM-Agent-First Specification (LAFS) as a standalone protocol repository.
4
+
5
+ This repository is language-neutral at the protocol layer and TypeScript-first for reference tooling.
6
+
7
+ ## What this repo provides
8
+
9
+ - Canonical protocol spec: `lafs.md`
10
+ - Versioned JSON schemas: `schemas/v1/`
11
+ - Error registry and transport mappings: `schemas/v1/error-registry.json`
12
+ - TypeScript validation/conformance toolkit: `src/`
13
+ - Automated conformance tests: `tests/`
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ```bash
24
+ npm run typecheck
25
+ npm test
26
+ npm run conformance -- --envelope fixtures/valid-success-envelope.json --flags fixtures/flags-valid.json
27
+ ```
28
+
29
+ ## Canonical policy
30
+
31
+ - JSON default output is REQUIRED.
32
+ - Human-readable output is explicit opt-in (`--human`).
33
+ - `--json` is optional but recommended explicit alias/override.
34
+ - `--human --json` is invalid and MUST fail with `E_FORMAT_CONFLICT`.
35
+
36
+ ## Layout
37
+
38
+ ```text
39
+ lafs.md
40
+ schemas/v1/
41
+ src/
42
+ tests/
43
+ fixtures/
44
+ docs/
45
+ ```
@@ -0,0 +1,193 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://lafs.dev/schemas/v1/envelope.schema.json",
4
+ "title": "LAFS Envelope v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "$schema",
9
+ "_meta",
10
+ "success",
11
+ "result",
12
+ "error",
13
+ "page"
14
+ ],
15
+ "properties": {
16
+ "$schema": {
17
+ "type": "string",
18
+ "const": "https://lafs.dev/schemas/v1/envelope.schema.json"
19
+ },
20
+ "_meta": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": [
24
+ "specVersion",
25
+ "schemaVersion",
26
+ "timestamp",
27
+ "operation",
28
+ "requestId",
29
+ "transport",
30
+ "strict",
31
+ "mvi",
32
+ "contextVersion"
33
+ ],
34
+ "properties": {
35
+ "specVersion": {
36
+ "type": "string",
37
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
38
+ },
39
+ "schemaVersion": {
40
+ "type": "string",
41
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
42
+ },
43
+ "timestamp": {
44
+ "type": "string",
45
+ "format": "date-time"
46
+ },
47
+ "operation": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "maxLength": 128
51
+ },
52
+ "requestId": {
53
+ "type": "string",
54
+ "minLength": 3,
55
+ "maxLength": 128
56
+ },
57
+ "transport": {
58
+ "type": "string",
59
+ "enum": ["cli", "http", "grpc", "sdk"]
60
+ },
61
+ "strict": {
62
+ "type": "boolean"
63
+ },
64
+ "mvi": {
65
+ "type": "boolean"
66
+ },
67
+ "contextVersion": {
68
+ "type": "integer",
69
+ "minimum": 0
70
+ }
71
+ }
72
+ },
73
+ "success": {
74
+ "type": "boolean"
75
+ },
76
+ "result": {
77
+ "type": ["object", "array", "null"]
78
+ },
79
+ "error": {
80
+ "type": ["object", "null"],
81
+ "additionalProperties": false,
82
+ "required": [
83
+ "code",
84
+ "message",
85
+ "category",
86
+ "retryable",
87
+ "retryAfterMs",
88
+ "details"
89
+ ],
90
+ "properties": {
91
+ "code": {
92
+ "type": "string",
93
+ "pattern": "^E_[A-Z0-9]+_[A-Z0-9_]+$"
94
+ },
95
+ "message": {
96
+ "type": "string",
97
+ "minLength": 1,
98
+ "maxLength": 1024
99
+ },
100
+ "category": {
101
+ "type": "string",
102
+ "enum": [
103
+ "VALIDATION",
104
+ "AUTH",
105
+ "PERMISSION",
106
+ "NOT_FOUND",
107
+ "CONFLICT",
108
+ "RATE_LIMIT",
109
+ "TRANSIENT",
110
+ "INTERNAL",
111
+ "CONTRACT",
112
+ "MIGRATION"
113
+ ]
114
+ },
115
+ "retryable": {
116
+ "type": "boolean"
117
+ },
118
+ "retryAfterMs": {
119
+ "type": ["integer", "null"],
120
+ "minimum": 0
121
+ },
122
+ "details": {
123
+ "type": "object"
124
+ }
125
+ }
126
+ },
127
+ "page": {
128
+ "type": ["object", "null"],
129
+ "additionalProperties": false,
130
+ "required": [
131
+ "mode",
132
+ "limit",
133
+ "offset",
134
+ "nextCursor",
135
+ "hasMore",
136
+ "total"
137
+ ],
138
+ "properties": {
139
+ "mode": {
140
+ "type": "string",
141
+ "enum": ["offset", "cursor", "none"]
142
+ },
143
+ "limit": {
144
+ "type": "integer",
145
+ "minimum": 1,
146
+ "maximum": 1000
147
+ },
148
+ "offset": {
149
+ "type": "integer",
150
+ "minimum": 0
151
+ },
152
+ "nextCursor": {
153
+ "type": ["string", "null"],
154
+ "maxLength": 2048
155
+ },
156
+ "hasMore": {
157
+ "type": "boolean"
158
+ },
159
+ "total": {
160
+ "type": ["integer", "null"],
161
+ "minimum": 0
162
+ }
163
+ }
164
+ }
165
+ },
166
+ "allOf": [
167
+ {
168
+ "if": {
169
+ "properties": {
170
+ "success": { "const": true }
171
+ }
172
+ },
173
+ "then": {
174
+ "properties": {
175
+ "error": { "type": "null" }
176
+ }
177
+ }
178
+ },
179
+ {
180
+ "if": {
181
+ "properties": {
182
+ "success": { "const": false }
183
+ }
184
+ },
185
+ "then": {
186
+ "properties": {
187
+ "result": { "type": "null" },
188
+ "error": { "type": "object" }
189
+ }
190
+ }
191
+ }
192
+ ]
193
+ }
@@ -0,0 +1,96 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "version": "1.0.0",
4
+ "codes": [
5
+ {
6
+ "code": "E_FORMAT_CONFLICT",
7
+ "category": "CONTRACT",
8
+ "description": "Mutually exclusive format flags requested",
9
+ "retryable": false,
10
+ "httpStatus": 400,
11
+ "grpcStatus": "INVALID_ARGUMENT",
12
+ "cliExit": 2
13
+ },
14
+ {
15
+ "code": "E_VALIDATION_SCHEMA",
16
+ "category": "VALIDATION",
17
+ "description": "Input failed schema validation",
18
+ "retryable": false,
19
+ "httpStatus": 400,
20
+ "grpcStatus": "INVALID_ARGUMENT",
21
+ "cliExit": 2
22
+ },
23
+ {
24
+ "code": "E_NOT_FOUND_RESOURCE",
25
+ "category": "NOT_FOUND",
26
+ "description": "Referenced resource was not found",
27
+ "retryable": false,
28
+ "httpStatus": 404,
29
+ "grpcStatus": "NOT_FOUND",
30
+ "cliExit": 4
31
+ },
32
+ {
33
+ "code": "E_CONFLICT_VERSION",
34
+ "category": "CONFLICT",
35
+ "description": "Version or concurrency conflict",
36
+ "retryable": true,
37
+ "httpStatus": 409,
38
+ "grpcStatus": "ABORTED",
39
+ "cliExit": 7
40
+ },
41
+ {
42
+ "code": "E_RATE_LIMITED",
43
+ "category": "RATE_LIMIT",
44
+ "description": "Rate limit exceeded",
45
+ "retryable": true,
46
+ "httpStatus": 429,
47
+ "grpcStatus": "RESOURCE_EXHAUSTED",
48
+ "cliExit": 8
49
+ },
50
+ {
51
+ "code": "E_TRANSIENT_UPSTREAM",
52
+ "category": "TRANSIENT",
53
+ "description": "Upstream dependency temporary failure",
54
+ "retryable": true,
55
+ "httpStatus": 503,
56
+ "grpcStatus": "UNAVAILABLE",
57
+ "cliExit": 9
58
+ },
59
+ {
60
+ "code": "E_INTERNAL_UNEXPECTED",
61
+ "category": "INTERNAL",
62
+ "description": "Unexpected internal failure",
63
+ "retryable": false,
64
+ "httpStatus": 500,
65
+ "grpcStatus": "INTERNAL",
66
+ "cliExit": 1
67
+ },
68
+ {
69
+ "code": "E_CONTEXT_MISSING",
70
+ "category": "CONTRACT",
71
+ "description": "Required context ledger fields are missing",
72
+ "retryable": false,
73
+ "httpStatus": 400,
74
+ "grpcStatus": "FAILED_PRECONDITION",
75
+ "cliExit": 6
76
+ },
77
+ {
78
+ "code": "E_CONTEXT_STALE",
79
+ "category": "CONFLICT",
80
+ "description": "Context ledger or references are stale",
81
+ "retryable": true,
82
+ "httpStatus": 409,
83
+ "grpcStatus": "ABORTED",
84
+ "cliExit": 7
85
+ },
86
+ {
87
+ "code": "E_MIGRATION_UNSUPPORTED_VERSION",
88
+ "category": "MIGRATION",
89
+ "description": "Requested protocol/schema version unsupported",
90
+ "retryable": false,
91
+ "httpStatus": 426,
92
+ "grpcStatus": "FAILED_PRECONDITION",
93
+ "cliExit": 10
94
+ }
95
+ ]
96
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ import { readFile } from "node:fs/promises";
3
+ import { runEnvelopeConformance, runFlagConformance } from "./conformance.js";
4
+ function parseArgs(argv) {
5
+ const args = {};
6
+ for (let i = 0; i < argv.length; i += 1) {
7
+ const current = argv[i];
8
+ const next = argv[i + 1];
9
+ if (current === "--envelope" && next) {
10
+ args.envelopePath = next;
11
+ i += 1;
12
+ }
13
+ else if (current === "--flags" && next) {
14
+ args.flagsPath = next;
15
+ i += 1;
16
+ }
17
+ }
18
+ return args;
19
+ }
20
+ async function readJson(path) {
21
+ const content = await readFile(path, "utf8");
22
+ return JSON.parse(content);
23
+ }
24
+ async function main() {
25
+ const args = parseArgs(process.argv.slice(2));
26
+ const reports = [];
27
+ if (args.envelopePath) {
28
+ const envelope = await readJson(args.envelopePath);
29
+ reports.push({ name: "envelope", report: runEnvelopeConformance(envelope) });
30
+ }
31
+ if (args.flagsPath) {
32
+ const flags = await readJson(args.flagsPath);
33
+ reports.push({ name: "flags", report: runFlagConformance(flags) });
34
+ }
35
+ if (reports.length === 0) {
36
+ throw new Error("Provide --envelope and/or --flags JSON files.");
37
+ }
38
+ console.log(JSON.stringify({ success: true, reports }, null, 2));
39
+ }
40
+ main().catch((error) => {
41
+ console.error(JSON.stringify({
42
+ success: false,
43
+ error: {
44
+ code: "E_INTERNAL_UNEXPECTED",
45
+ message: error instanceof Error ? error.message : String(error)
46
+ }
47
+ }, null, 2));
48
+ process.exit(1);
49
+ });
@@ -0,0 +1,3 @@
1
+ import type { ConformanceReport, FlagInput } from "./types.js";
2
+ export declare function runEnvelopeConformance(envelope: unknown): ConformanceReport;
3
+ export declare function runFlagConformance(flags: FlagInput): ConformanceReport;
@@ -0,0 +1,45 @@
1
+ import { isRegisteredErrorCode } from "./errorRegistry.js";
2
+ import { resolveOutputFormat, LAFSFlagError } from "./flagSemantics.js";
3
+ import { validateEnvelope } from "./validateEnvelope.js";
4
+ function pushCheck(checks, name, pass, detail) {
5
+ checks.push({ name, pass, ...(detail ? { detail } : {}) });
6
+ }
7
+ export function runEnvelopeConformance(envelope) {
8
+ const checks = [];
9
+ const validation = validateEnvelope(envelope);
10
+ pushCheck(checks, "envelope_schema_valid", validation.valid, validation.valid ? undefined : validation.errors.join("; "));
11
+ if (!validation.valid) {
12
+ return { ok: false, checks };
13
+ }
14
+ const typed = envelope;
15
+ const invariant = typed.success ? typed.error === null : typed.result === null;
16
+ pushCheck(checks, "envelope_invariants", invariant, invariant ? undefined : "success/result/error invariant violated");
17
+ if (typed.error) {
18
+ const registered = isRegisteredErrorCode(typed.error.code);
19
+ pushCheck(checks, "error_code_registered", registered, registered ? undefined : `unregistered code: ${typed.error.code}`);
20
+ }
21
+ else {
22
+ pushCheck(checks, "error_code_registered", true);
23
+ }
24
+ pushCheck(checks, "meta_mvi_present", typeof typed._meta.mvi === "boolean");
25
+ pushCheck(checks, "meta_strict_present", typeof typed._meta.strict === "boolean");
26
+ return { ok: checks.every((check) => check.pass), checks };
27
+ }
28
+ export function runFlagConformance(flags) {
29
+ const checks = [];
30
+ try {
31
+ const resolved = resolveOutputFormat(flags);
32
+ pushCheck(checks, "flag_conflict_rejected", !(flags.humanFlag && flags.jsonFlag));
33
+ pushCheck(checks, "json_default_when_unspecified", resolved.format === "json" || Boolean(flags.projectDefault || flags.userDefault));
34
+ }
35
+ catch (error) {
36
+ if (error instanceof LAFSFlagError && error.code === "E_FORMAT_CONFLICT") {
37
+ pushCheck(checks, "flag_conflict_rejected", true);
38
+ pushCheck(checks, "json_default_when_unspecified", true);
39
+ return { ok: true, checks };
40
+ }
41
+ pushCheck(checks, "flag_resolution", false, error instanceof Error ? error.message : String(error));
42
+ return { ok: false, checks };
43
+ }
44
+ return { ok: checks.every((check) => check.pass), checks };
45
+ }
@@ -0,0 +1,15 @@
1
+ export interface RegistryCode {
2
+ code: string;
3
+ category: string;
4
+ description: string;
5
+ retryable: boolean;
6
+ httpStatus: number;
7
+ grpcStatus: string;
8
+ cliExit: number;
9
+ }
10
+ export interface ErrorRegistry {
11
+ version: string;
12
+ codes: RegistryCode[];
13
+ }
14
+ export declare function getErrorRegistry(): ErrorRegistry;
15
+ export declare function isRegisteredErrorCode(code: string): boolean;
@@ -0,0 +1,8 @@
1
+ import errorRegistry from "../schemas/v1/error-registry.json" with { type: "json" };
2
+ export function getErrorRegistry() {
3
+ return errorRegistry;
4
+ }
5
+ export function isRegisteredErrorCode(code) {
6
+ const registry = getErrorRegistry();
7
+ return registry.codes.some((item) => item.code === code);
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { FlagInput } from "./types.js";
2
+ export interface FlagResolution {
3
+ format: "json" | "human";
4
+ source: "flag" | "project" | "user" | "default";
5
+ }
6
+ export declare class LAFSFlagError extends Error {
7
+ code: string;
8
+ constructor(code: string, message: string);
9
+ }
10
+ export declare function resolveOutputFormat(input: FlagInput): FlagResolution;
@@ -0,0 +1,29 @@
1
+ export class LAFSFlagError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = "LAFSFlagError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export function resolveOutputFormat(input) {
10
+ if (input.humanFlag && input.jsonFlag) {
11
+ throw new LAFSFlagError("E_FORMAT_CONFLICT", "Cannot combine --human and --json in the same invocation.");
12
+ }
13
+ if (input.requestedFormat) {
14
+ return { format: input.requestedFormat, source: "flag" };
15
+ }
16
+ if (input.humanFlag) {
17
+ return { format: "human", source: "flag" };
18
+ }
19
+ if (input.jsonFlag) {
20
+ return { format: "json", source: "flag" };
21
+ }
22
+ if (input.projectDefault) {
23
+ return { format: input.projectDefault, source: "project" };
24
+ }
25
+ if (input.userDefault) {
26
+ return { format: input.userDefault, source: "user" };
27
+ }
28
+ return { format: "json", source: "default" };
29
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./errorRegistry.js";
3
+ export * from "./validateEnvelope.js";
4
+ export * from "./flagSemantics.js";
5
+ export * from "./conformance.js";
@@ -0,0 +1,5 @@
1
+ export * from "./types.js";
2
+ export * from "./errorRegistry.js";
3
+ export * from "./validateEnvelope.js";
4
+ export * from "./flagSemantics.js";
5
+ export * from "./conformance.js";
@@ -0,0 +1,52 @@
1
+ export type LAFSTransport = "cli" | "http" | "grpc" | "sdk";
2
+ export type LAFSErrorCategory = "VALIDATION" | "AUTH" | "PERMISSION" | "NOT_FOUND" | "CONFLICT" | "RATE_LIMIT" | "TRANSIENT" | "INTERNAL" | "CONTRACT" | "MIGRATION";
3
+ export interface LAFSMeta {
4
+ specVersion: string;
5
+ schemaVersion: string;
6
+ timestamp: string;
7
+ operation: string;
8
+ requestId: string;
9
+ transport: LAFSTransport;
10
+ strict: boolean;
11
+ mvi: boolean;
12
+ contextVersion: number;
13
+ }
14
+ export interface LAFSError {
15
+ code: string;
16
+ message: string;
17
+ category: LAFSErrorCategory;
18
+ retryable: boolean;
19
+ retryAfterMs: number | null;
20
+ details: Record<string, unknown>;
21
+ }
22
+ export interface LAFSPage {
23
+ mode: "offset" | "cursor" | "none";
24
+ limit: number;
25
+ offset: number;
26
+ nextCursor: string | null;
27
+ hasMore: boolean;
28
+ total: number | null;
29
+ }
30
+ export interface LAFSEnvelope {
31
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json";
32
+ _meta: LAFSMeta;
33
+ success: boolean;
34
+ result: Record<string, unknown> | Record<string, unknown>[] | null;
35
+ error: LAFSError | null;
36
+ page: LAFSPage | null;
37
+ }
38
+ export interface FlagInput {
39
+ requestedFormat?: "json" | "human";
40
+ jsonFlag?: boolean;
41
+ humanFlag?: boolean;
42
+ projectDefault?: "json" | "human";
43
+ userDefault?: "json" | "human";
44
+ }
45
+ export interface ConformanceReport {
46
+ ok: boolean;
47
+ checks: Array<{
48
+ name: string;
49
+ pass: boolean;
50
+ detail?: string;
51
+ }>;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { LAFSEnvelope } from "./types.js";
2
+ export interface EnvelopeValidationResult {
3
+ valid: boolean;
4
+ errors: string[];
5
+ }
6
+ export declare function validateEnvelope(input: unknown): EnvelopeValidationResult;
7
+ export declare function assertEnvelope(input: unknown): LAFSEnvelope;
@@ -0,0 +1,28 @@
1
+ import { createRequire } from "node:module";
2
+ import envelopeSchema from "../schemas/v1/envelope.schema.json" with { type: "json" };
3
+ const require = createRequire(import.meta.url);
4
+ const AjvModule = require("ajv");
5
+ const AddFormatsModule = require("ajv-formats");
6
+ const AjvCtor = (typeof AjvModule === "function" ? AjvModule : AjvModule.default);
7
+ const addFormats = (typeof AddFormatsModule === "function" ? AddFormatsModule : AddFormatsModule.default);
8
+ const ajv = new AjvCtor({ allErrors: true, strict: true, allowUnionTypes: true });
9
+ addFormats(ajv);
10
+ const validate = ajv.compile(envelopeSchema);
11
+ export function validateEnvelope(input) {
12
+ const valid = validate(input);
13
+ if (valid) {
14
+ return { valid: true, errors: [] };
15
+ }
16
+ const errors = (validate.errors ?? []).map((error) => {
17
+ const path = error.instancePath || "/";
18
+ return `${path} ${error.message ?? "validation error"}`.trim();
19
+ });
20
+ return { valid: false, errors };
21
+ }
22
+ export function assertEnvelope(input) {
23
+ const result = validateEnvelope(input);
24
+ if (!result.valid) {
25
+ throw new Error(`Invalid LAFS envelope: ${result.errors.join("; ")}`);
26
+ }
27
+ return input;
28
+ }
package/lafs.md ADDED
@@ -0,0 +1,169 @@
1
+ # LAFS: LLM-Agent-First Specification
2
+
3
+ ## 1. Scope
4
+
5
+ LAFS defines a protocol for software systems whose primary consumer is an LLM agent.
6
+
7
+ LAFS is transport-agnostic and language-agnostic. It applies to CLI, SDK, HTTP, gRPC, and orchestrated multi-agent systems.
8
+
9
+ ---
10
+
11
+ ## 2. RFC 2119 Keywords
12
+
13
+ The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are interpreted per RFC 2119.
14
+
15
+ ---
16
+
17
+ ## 3. Non-Negotiable Protocol Rules
18
+
19
+ 1. Output default MUST be machine-readable JSON.
20
+ 2. Human-readable mode MUST be explicit opt-in.
21
+ 3. Context continuity MUST be preserved across steps.
22
+ 4. MVI (Minimal Viable Information) MUST be default response behavior.
23
+ 5. Progressive disclosure MUST be used for expanded detail retrieval.
24
+ 6. Contracts MUST be deterministic and testable.
25
+
26
+ ---
27
+
28
+ ## 4. Format Semantics
29
+
30
+ ### 4.1 Required output semantics
31
+
32
+ - Default format MUST be `json`.
33
+ - `--human` MUST switch output mode to human-readable.
34
+ - `--json` MAY be supported as explicit alias/override and is RECOMMENDED.
35
+ - Providing both `--human` and `--json` MUST fail with `E_FORMAT_CONFLICT`.
36
+ - Explicit flags MUST override env/config defaults.
37
+
38
+ ### 4.2 Recommended precedence
39
+
40
+ 1. Explicit CLI/API request value
41
+ 2. Project config
42
+ 3. Global/user config
43
+ 4. Protocol default (`json`)
44
+
45
+ ---
46
+
47
+ ## 5. Canonical Response Envelope
48
+
49
+ All responses MUST conform to `schemas/v1/envelope.schema.json`.
50
+
51
+ ```json
52
+ {
53
+ "$schema": "https://lafs.dev/schemas/v1/envelope.schema.json",
54
+ "_meta": {
55
+ "specVersion": "1.0.0",
56
+ "schemaVersion": "1.0.0",
57
+ "timestamp": "2026-02-11T00:00:00Z",
58
+ "operation": "operation.name",
59
+ "requestId": "req_123",
60
+ "transport": "cli",
61
+ "strict": true,
62
+ "mvi": true,
63
+ "contextVersion": 0
64
+ },
65
+ "success": true,
66
+ "result": {},
67
+ "error": null,
68
+ "page": null
69
+ }
70
+ ```
71
+
72
+ ### 5.1 Envelope invariants
73
+
74
+ - Exactly one of `result` or `error` MUST be non-null.
75
+ - `success=true` implies `error=null`.
76
+ - `success=false` implies `result=null`.
77
+ - Unknown fields SHOULD be rejected when strict mode is enabled.
78
+
79
+ ---
80
+
81
+ ## 6. Error Contract
82
+
83
+ Errors MUST conform to envelope `error` shape and use codes from `schemas/v1/error-registry.json`.
84
+
85
+ ```json
86
+ {
87
+ "code": "E_VALIDATION_SCHEMA",
88
+ "message": "Invalid input payload",
89
+ "category": "VALIDATION",
90
+ "retryable": false,
91
+ "retryAfterMs": null,
92
+ "details": {
93
+ "field": "limit"
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### 6.1 Required behavior
99
+
100
+ - Error codes MUST be stable within major versions.
101
+ - Retry semantics MUST be encoded in `retryable` and `retryAfterMs`.
102
+ - CLI/HTTP/gRPC mappings SHOULD follow the registry.
103
+
104
+ ---
105
+
106
+ ## 7. Context Preservation
107
+
108
+ Multi-step operations MUST preserve a context ledger with at least:
109
+
110
+ - `objective`
111
+ - `constraints[]`
112
+ - `references[]`
113
+ - `decisions[]`
114
+ - `openIssues[]`
115
+ - `state`
116
+ - `version`
117
+
118
+ Rules:
119
+
120
+ - Version MUST increase monotonically by 1 for accepted mutations.
121
+ - Accepted active constraints MUST NOT be silently removed.
122
+ - Decisions affecting output MUST be represented in ledger state.
123
+ - Missing required context for a mutating step MUST fail with structured error.
124
+
125
+ ---
126
+
127
+ ## 8. MVI and Progressive Disclosure
128
+
129
+ ### 8.1 MVI default
130
+
131
+ - Default list/batch outputs MUST only contain fields required for next action.
132
+ - Verbose fields SHOULD be omitted by default.
133
+ - Systems SHOULD publish operation-level MVI budgets.
134
+
135
+ ### 8.2 Progressive disclosure
136
+
137
+ - Expanded detail retrieval MUST require explicit request.
138
+ - Unknown expansion fields SHOULD fail validation.
139
+
140
+ ### 8.3 Pagination
141
+
142
+ - List operations SHOULD return deterministic `page` metadata.
143
+ - Pagination mode (offset or cursor) MUST be documented.
144
+ - Mixed pagination modes in one request MUST fail validation.
145
+
146
+ ---
147
+
148
+ ## 9. Strictness
149
+
150
+ - Agent surfaces SHOULD default `strict=true`.
151
+ - Strict mode violations SHOULD fail with contract/validation error codes.
152
+ - Response metadata MUST expose strict mode status.
153
+
154
+ ---
155
+
156
+ ## 10. Versioning and Deprecation
157
+
158
+ - Protocol versions MUST follow SemVer.
159
+ - Minor/patch changes MUST be backward compatible.
160
+ - Breaking changes MUST require major version increments.
161
+ - Deprecated fields MUST have documented sunset policy.
162
+
163
+ See `docs/VERSIONING.md` and `docs/DEPRECATION.md`.
164
+
165
+ ---
166
+
167
+ ## 11. Conformance
168
+
169
+ Conforming implementations MUST pass minimum checks in `docs/CONFORMANCE.md` and schema validation for the canonical envelope.
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@cleocode/lafs-protocol",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "LLM-Agent-First Specification schemas and conformance tooling",
7
+ "main": "dist/src/index.js",
8
+ "types": "dist/src/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "schemas",
12
+ "lafs.md",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/kryptobaseddev/lafs-protocol.git"
19
+ },
20
+ "homepage": "https://github.com/kryptobaseddev/lafs-protocol#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/kryptobaseddev/lafs-protocol/issues"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "bin": {
28
+ "lafs-conformance": "dist/src/cli.js"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run",
34
+ "conformance": "tsx src/cli.ts"
35
+ },
36
+ "keywords": [
37
+ "lafs",
38
+ "llm",
39
+ "agent",
40
+ "protocol",
41
+ "schema",
42
+ "conformance"
43
+ ],
44
+ "license": "MIT",
45
+ "devDependencies": {
46
+ "@types/node": "^24.3.0",
47
+ "tsx": "^4.20.5",
48
+ "typescript": "^5.9.2",
49
+ "vitest": "^2.1.9"
50
+ },
51
+ "dependencies": {
52
+ "ajv": "^8.17.1",
53
+ "ajv-formats": "^3.0.1"
54
+ }
55
+ }
@@ -0,0 +1,193 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://lafs.dev/schemas/v1/envelope.schema.json",
4
+ "title": "LAFS Envelope v1",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": [
8
+ "$schema",
9
+ "_meta",
10
+ "success",
11
+ "result",
12
+ "error",
13
+ "page"
14
+ ],
15
+ "properties": {
16
+ "$schema": {
17
+ "type": "string",
18
+ "const": "https://lafs.dev/schemas/v1/envelope.schema.json"
19
+ },
20
+ "_meta": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": [
24
+ "specVersion",
25
+ "schemaVersion",
26
+ "timestamp",
27
+ "operation",
28
+ "requestId",
29
+ "transport",
30
+ "strict",
31
+ "mvi",
32
+ "contextVersion"
33
+ ],
34
+ "properties": {
35
+ "specVersion": {
36
+ "type": "string",
37
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
38
+ },
39
+ "schemaVersion": {
40
+ "type": "string",
41
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
42
+ },
43
+ "timestamp": {
44
+ "type": "string",
45
+ "format": "date-time"
46
+ },
47
+ "operation": {
48
+ "type": "string",
49
+ "minLength": 1,
50
+ "maxLength": 128
51
+ },
52
+ "requestId": {
53
+ "type": "string",
54
+ "minLength": 3,
55
+ "maxLength": 128
56
+ },
57
+ "transport": {
58
+ "type": "string",
59
+ "enum": ["cli", "http", "grpc", "sdk"]
60
+ },
61
+ "strict": {
62
+ "type": "boolean"
63
+ },
64
+ "mvi": {
65
+ "type": "boolean"
66
+ },
67
+ "contextVersion": {
68
+ "type": "integer",
69
+ "minimum": 0
70
+ }
71
+ }
72
+ },
73
+ "success": {
74
+ "type": "boolean"
75
+ },
76
+ "result": {
77
+ "type": ["object", "array", "null"]
78
+ },
79
+ "error": {
80
+ "type": ["object", "null"],
81
+ "additionalProperties": false,
82
+ "required": [
83
+ "code",
84
+ "message",
85
+ "category",
86
+ "retryable",
87
+ "retryAfterMs",
88
+ "details"
89
+ ],
90
+ "properties": {
91
+ "code": {
92
+ "type": "string",
93
+ "pattern": "^E_[A-Z0-9]+_[A-Z0-9_]+$"
94
+ },
95
+ "message": {
96
+ "type": "string",
97
+ "minLength": 1,
98
+ "maxLength": 1024
99
+ },
100
+ "category": {
101
+ "type": "string",
102
+ "enum": [
103
+ "VALIDATION",
104
+ "AUTH",
105
+ "PERMISSION",
106
+ "NOT_FOUND",
107
+ "CONFLICT",
108
+ "RATE_LIMIT",
109
+ "TRANSIENT",
110
+ "INTERNAL",
111
+ "CONTRACT",
112
+ "MIGRATION"
113
+ ]
114
+ },
115
+ "retryable": {
116
+ "type": "boolean"
117
+ },
118
+ "retryAfterMs": {
119
+ "type": ["integer", "null"],
120
+ "minimum": 0
121
+ },
122
+ "details": {
123
+ "type": "object"
124
+ }
125
+ }
126
+ },
127
+ "page": {
128
+ "type": ["object", "null"],
129
+ "additionalProperties": false,
130
+ "required": [
131
+ "mode",
132
+ "limit",
133
+ "offset",
134
+ "nextCursor",
135
+ "hasMore",
136
+ "total"
137
+ ],
138
+ "properties": {
139
+ "mode": {
140
+ "type": "string",
141
+ "enum": ["offset", "cursor", "none"]
142
+ },
143
+ "limit": {
144
+ "type": "integer",
145
+ "minimum": 1,
146
+ "maximum": 1000
147
+ },
148
+ "offset": {
149
+ "type": "integer",
150
+ "minimum": 0
151
+ },
152
+ "nextCursor": {
153
+ "type": ["string", "null"],
154
+ "maxLength": 2048
155
+ },
156
+ "hasMore": {
157
+ "type": "boolean"
158
+ },
159
+ "total": {
160
+ "type": ["integer", "null"],
161
+ "minimum": 0
162
+ }
163
+ }
164
+ }
165
+ },
166
+ "allOf": [
167
+ {
168
+ "if": {
169
+ "properties": {
170
+ "success": { "const": true }
171
+ }
172
+ },
173
+ "then": {
174
+ "properties": {
175
+ "error": { "type": "null" }
176
+ }
177
+ }
178
+ },
179
+ {
180
+ "if": {
181
+ "properties": {
182
+ "success": { "const": false }
183
+ }
184
+ },
185
+ "then": {
186
+ "properties": {
187
+ "result": { "type": "null" },
188
+ "error": { "type": "object" }
189
+ }
190
+ }
191
+ }
192
+ ]
193
+ }
@@ -0,0 +1,96 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "version": "1.0.0",
4
+ "codes": [
5
+ {
6
+ "code": "E_FORMAT_CONFLICT",
7
+ "category": "CONTRACT",
8
+ "description": "Mutually exclusive format flags requested",
9
+ "retryable": false,
10
+ "httpStatus": 400,
11
+ "grpcStatus": "INVALID_ARGUMENT",
12
+ "cliExit": 2
13
+ },
14
+ {
15
+ "code": "E_VALIDATION_SCHEMA",
16
+ "category": "VALIDATION",
17
+ "description": "Input failed schema validation",
18
+ "retryable": false,
19
+ "httpStatus": 400,
20
+ "grpcStatus": "INVALID_ARGUMENT",
21
+ "cliExit": 2
22
+ },
23
+ {
24
+ "code": "E_NOT_FOUND_RESOURCE",
25
+ "category": "NOT_FOUND",
26
+ "description": "Referenced resource was not found",
27
+ "retryable": false,
28
+ "httpStatus": 404,
29
+ "grpcStatus": "NOT_FOUND",
30
+ "cliExit": 4
31
+ },
32
+ {
33
+ "code": "E_CONFLICT_VERSION",
34
+ "category": "CONFLICT",
35
+ "description": "Version or concurrency conflict",
36
+ "retryable": true,
37
+ "httpStatus": 409,
38
+ "grpcStatus": "ABORTED",
39
+ "cliExit": 7
40
+ },
41
+ {
42
+ "code": "E_RATE_LIMITED",
43
+ "category": "RATE_LIMIT",
44
+ "description": "Rate limit exceeded",
45
+ "retryable": true,
46
+ "httpStatus": 429,
47
+ "grpcStatus": "RESOURCE_EXHAUSTED",
48
+ "cliExit": 8
49
+ },
50
+ {
51
+ "code": "E_TRANSIENT_UPSTREAM",
52
+ "category": "TRANSIENT",
53
+ "description": "Upstream dependency temporary failure",
54
+ "retryable": true,
55
+ "httpStatus": 503,
56
+ "grpcStatus": "UNAVAILABLE",
57
+ "cliExit": 9
58
+ },
59
+ {
60
+ "code": "E_INTERNAL_UNEXPECTED",
61
+ "category": "INTERNAL",
62
+ "description": "Unexpected internal failure",
63
+ "retryable": false,
64
+ "httpStatus": 500,
65
+ "grpcStatus": "INTERNAL",
66
+ "cliExit": 1
67
+ },
68
+ {
69
+ "code": "E_CONTEXT_MISSING",
70
+ "category": "CONTRACT",
71
+ "description": "Required context ledger fields are missing",
72
+ "retryable": false,
73
+ "httpStatus": 400,
74
+ "grpcStatus": "FAILED_PRECONDITION",
75
+ "cliExit": 6
76
+ },
77
+ {
78
+ "code": "E_CONTEXT_STALE",
79
+ "category": "CONFLICT",
80
+ "description": "Context ledger or references are stale",
81
+ "retryable": true,
82
+ "httpStatus": 409,
83
+ "grpcStatus": "ABORTED",
84
+ "cliExit": 7
85
+ },
86
+ {
87
+ "code": "E_MIGRATION_UNSUPPORTED_VERSION",
88
+ "category": "MIGRATION",
89
+ "description": "Requested protocol/schema version unsupported",
90
+ "retryable": false,
91
+ "httpStatus": 426,
92
+ "grpcStatus": "FAILED_PRECONDITION",
93
+ "cliExit": 10
94
+ }
95
+ ]
96
+ }