@cleocode/cant 2026.5.133 → 2026.5.134

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/README.md CHANGED
@@ -19,9 +19,15 @@ pnpm add @cleocode/cant
19
19
 
20
20
  The package ships pre-built napi binaries via `optionalDependencies` for
21
21
  `x86_64-unknown-linux-gnu`. On other platforms `@cleocode/cant` falls back to
22
- graceful errors at runtime — the TypeScript surface still loads but
22
+ typed errors at runtime — the TypeScript surface still loads but
23
23
  parser/validator/executor calls require a native binding present.
24
24
 
25
+ **`cant-core` is the single source of truth (SSoT) for all CANT parsing.**
26
+ There is no JS-regex fallback parser in the routine code path. If the native
27
+ addon is absent, `parseCANTMessage` throws a typed error unless the
28
+ `CLEO_CANT_ALLOW_JS_FALLBACK=1` env-var is explicitly set (degraded mode,
29
+ not for production use).
30
+
25
31
  ## Public API
26
32
 
27
33
  ```ts
@@ -144,9 +150,19 @@ Requires a Rust toolchain. Produces `napi/cant.linux-x64-gnu.node`.
144
150
  Other platform triples are added via the workspace release pipeline,
145
151
  not locally.
146
152
 
153
+ ## Crate track
154
+
155
+ | Crate | Status | Purpose |
156
+ |-------|--------|---------|
157
+ | `crates/cant-core` | Active — SSoT | Parser, validator, 42 static-analysis rules, pipeline executor |
158
+ | `crates/cant-napi` | Active | napi-rs cdylib binding wrapping cant-core + cant-runtime |
159
+ | `crates/cant-runtime` | Active | Deterministic pipeline execution engine (Path B) |
160
+ | `crates/cant-router` | Retired from napi surface (E8 T11432) | Model-tier classifier — preserved in workspace but NOT linked into the published binary |
161
+ | `crates/cant-lsp` | Shelved from default build (E8 T11434) | Language Server Protocol for `.cant` files — kept in workspace, build with `cargo build -p cant-lsp` |
162
+
147
163
  ## Related
148
164
 
149
- - [`crates/cant-core`](../../crates/cant-core) — Rust source of truth (parser, validator, executor)
165
+ - [`crates/cant-core`](../../crates/cant-core) — Rust SSoT (parser, validator, pipeline executor)
150
166
  - [`crates/cant-napi`](../../crates/cant-napi) — napi-rs bindings (cdylib)
151
167
  - [`packages/cleo/templates/cleoos-hub/pi-extensions/cant-bridge.ts`](../cleo/templates/cleoos-hub/pi-extensions/cant-bridge.ts) — Pi-interactive runtime (Path A)
152
168
  - [`.cleo/adrs/ADR-035-pi-v2-v3-harness.md`](../../.cleo/adrs/ADR-035-pi-v2-v3-harness.md) — architecture decisions for the CANT execution model
@@ -270,8 +270,10 @@ export type SeedPersonaId = (typeof SEED_PERSONA_IDS)[number];
270
270
  * Files that cannot be parsed (unreadable, missing `agent <id>:` block) are
271
271
  * silently skipped.
272
272
  *
273
- * This function is intentionally pure-TS and does NOT require the native addon.
274
- * It is safe to call in environments where `cant-napi` is absent (CI, test, etc.).
273
+ * When the native addon is available, agent identity fields are extracted via
274
+ * `cant_extract_agent_profiles` (cant-core, E8-AC2 / T11430). When the addon
275
+ * is absent the loader falls back to a minimal regex extractor so basic
276
+ * identity resolution still works in environments without the binary.
275
277
  *
276
278
  * @param agentsRoot - Optional override for the `packages/agents/` root. When
277
279
  * omitted the path is resolved automatically relative to this file. Tests
@@ -309,22 +309,6 @@ function resolveAgentsPackageRoot() {
309
309
  ];
310
310
  return candidates.find((p) => (0, node_fs_1.existsSync)(p)) ?? null;
311
311
  }
312
- /**
313
- * Extract the `role:` value from a raw `.cant` file body.
314
- *
315
- * Looks for a line matching ` role: <value>` within the `agent <id>:` block
316
- * using a simple regex — intentionally avoids full CANT parsing to remove the
317
- * native-addon dependency from this path (the addon may not be present in all
318
- * environments where the loader runs).
319
- *
320
- * @param content - Raw `.cant` file text.
321
- * @returns Extracted role string, or `null` when no `role:` line is found.
322
- * @internal
323
- */
324
- function extractRoleFromCant(content) {
325
- const match = /^\s{2}role:\s*(\S+)/m.exec(content);
326
- return match ? (match[1] ?? null) : null;
327
- }
328
312
  /**
329
313
  * Map a raw role string from a `.cant` file to a {@link PeerKind}.
330
314
  *
@@ -347,61 +331,22 @@ function roleToPeerKind(rawRole) {
347
331
  }
348
332
  }
349
333
  /**
350
- * Extract the `description:` value from a raw `.cant` file body.
351
- *
352
- * Handles single-line descriptions (`description: "..."`) and multiline
353
- * block-scalar descriptions (`description: |`). Returns the first line of
354
- * the block-scalar body for multiline values. Returns empty string when no
355
- * `description:` field is found.
356
- *
357
- * @param content - Raw `.cant` file text.
358
- * @internal
359
- */
360
- function extractDescriptionFromCant(content) {
361
- // Single-line: ` description: "some text"` or ` description: some text`
362
- const singleLine = /^\s{2}description:\s+"([^"]+)"/m.exec(content);
363
- if (singleLine)
364
- return singleLine[1] ?? '';
365
- const singleLineUnquoted = /^\s{2}description:\s+(.+)/m.exec(content);
366
- if (singleLineUnquoted) {
367
- const raw = singleLineUnquoted[1] ?? '';
368
- // Exclude multiline indicator '|'
369
- if (raw.trim() !== '|')
370
- return raw.trim();
371
- }
372
- // Multiline block scalar: grab first non-empty line after `description: |`
373
- const blockScalar = /^\s{2}description:\s+\|\s*\n((?:\s+\S[^\n]*\n?)+)/m.exec(content);
374
- if (blockScalar) {
375
- const bodyLines = (blockScalar[1] ?? '').split('\n');
376
- for (const line of bodyLines) {
377
- const trimmed = line.trim();
378
- if (trimmed.length > 0)
379
- return trimmed;
380
- }
381
- }
382
- return '';
383
- }
384
- /**
385
- * Extract the agent business id from a raw `.cant` file body.
334
+ * Parse a single `.cant` file into a {@link PeerIdentity} via the canonical
335
+ * `cant_extract_agent_profiles` napi path (E8-AC2, T11430).
386
336
  *
387
- * Looks for the `agent <id>:` declaration line. Returns `null` when not found.
337
+ * When the native addon is available, agent id, role, and description are
338
+ * extracted by the Rust cant-core parser — eliminating the previous
339
+ * regex-based `extractRoleFromCant` / `extractAgentIdFromCant` /
340
+ * `extractDescriptionFromCant` helpers which are now retired.
388
341
  *
389
- * @param content - Raw `.cant` file text.
390
- * @internal
391
- */
392
- function extractAgentIdFromCant(content) {
393
- const match = /^agent\s+([a-z][a-z0-9-]*):/m.exec(content);
394
- return match ? (match[1] ?? null) : null;
395
- }
396
- /**
397
- * Parse a single `.cant` file at `cantFile` into a {@link PeerIdentity}.
398
- *
399
- * Returns `null` when the file cannot be parsed (missing `agent <id>:` block
400
- * or unreadable file). The caller is responsible for logging / skipping nulls.
342
+ * Falls back to a minimal regex extractor ONLY when the native addon is
343
+ * absent AND `fallbackId` is provided (i.e. the universal-base path where
344
+ * the filename is the canonical id). Returns `null` when the file is
345
+ * unreadable or no agent id can be determined.
401
346
  *
402
347
  * @param cantFile - Absolute path to the `.cant` file.
403
- * @param fallbackId - Id to use when the `agent <id>:` block is absent (e.g.,
404
- * when loading the universal base by a known filename).
348
+ * @param fallbackId - Id to use when no `agent <id>:` block can be parsed
349
+ * (e.g. when loading the universal base by a known filename).
405
350
  * @internal
406
351
  */
407
352
  function parseCantFileToIdentity(cantFile, fallbackId) {
@@ -412,18 +357,77 @@ function parseCantFileToIdentity(cantFile, fallbackId) {
412
357
  catch {
413
358
  return null;
414
359
  }
415
- const agentId = extractAgentIdFromCant(content) ?? fallbackId ?? null;
360
+ // Primary path: route through the cant-core napi bridge.
361
+ // The native `cantExtractAgentProfiles` returns loose objects whose shape
362
+ // mirrors the Rust AgentProfile extractor. Common observed fields:
363
+ // - `name` — agent business id (kebab-case)
364
+ // - `agentId` — alternative agent id field (if present)
365
+ // - `role` — role string (may also live inside `propertiesJson`)
366
+ // - `description` — agent description (may also live inside `propertiesJson`)
367
+ // - `propertiesJson` — JSON-serialized top-level properties blob
368
+ if (isNativeAvailable()) {
369
+ try {
370
+ const raw = cantExtractAgentProfilesNative(content);
371
+ if (Array.isArray(raw) && raw.length > 0) {
372
+ const entry = raw[0];
373
+ // Resolve agent id: prefer explicit agentId, fall back to name, then fallbackId.
374
+ const agentId = (typeof entry['agentId'] === 'string' ? entry['agentId'] : undefined) ??
375
+ (typeof entry['name'] === 'string' ? entry['name'] : undefined) ??
376
+ fallbackId ??
377
+ null;
378
+ if (!agentId)
379
+ return null;
380
+ // Resolve role: try direct field, then propertiesJson.
381
+ let rawRole = typeof entry['role'] === 'string' ? entry['role'] : null;
382
+ if (!rawRole && typeof entry['propertiesJson'] === 'string') {
383
+ try {
384
+ const props = JSON.parse(entry['propertiesJson']);
385
+ rawRole = typeof props['role'] === 'string' ? props['role'] : null;
386
+ }
387
+ catch {
388
+ // ignore malformed JSON
389
+ }
390
+ }
391
+ const peerKind = roleToPeerKind(rawRole);
392
+ // Resolve description: try direct field, then propertiesJson.
393
+ let description = typeof entry['description'] === 'string' ? entry['description'] : '';
394
+ if (!description && typeof entry['propertiesJson'] === 'string') {
395
+ try {
396
+ const props = JSON.parse(entry['propertiesJson']);
397
+ description = typeof props['description'] === 'string' ? props['description'] : '';
398
+ }
399
+ catch {
400
+ // ignore malformed JSON
401
+ }
402
+ }
403
+ return {
404
+ peerId: agentId,
405
+ peerKind,
406
+ cantFile,
407
+ displayName: agentId,
408
+ description,
409
+ };
410
+ }
411
+ }
412
+ catch {
413
+ // Fall through to regex fallback below.
414
+ }
415
+ }
416
+ // Degraded regex fallback — only used when the native addon is absent.
417
+ // Extracts the minimum fields needed to produce a usable PeerIdentity.
418
+ const agentIdMatch = /^agent\s+([a-z][a-z0-9-]*):/m.exec(content);
419
+ const agentId = (agentIdMatch !== null ? agentIdMatch[1] : undefined) ?? fallbackId ?? null;
416
420
  if (!agentId)
417
421
  return null;
418
- const rawRole = extractRoleFromCant(content);
422
+ const roleMatch = /^\s{2}role:\s*(\S+)/m.exec(content);
423
+ const rawRole = roleMatch ? (roleMatch[1] ?? null) : null;
419
424
  const peerKind = roleToPeerKind(rawRole);
420
- const description = extractDescriptionFromCant(content);
421
425
  return {
422
426
  peerId: agentId,
423
427
  peerKind,
424
428
  cantFile,
425
429
  displayName: agentId,
426
- description,
430
+ description: '',
427
431
  };
428
432
  }
429
433
  /**
@@ -438,8 +442,10 @@ function parseCantFileToIdentity(cantFile, fallbackId) {
438
442
  * Files that cannot be parsed (unreadable, missing `agent <id>:` block) are
439
443
  * silently skipped.
440
444
  *
441
- * This function is intentionally pure-TS and does NOT require the native addon.
442
- * It is safe to call in environments where `cant-napi` is absent (CI, test, etc.).
445
+ * When the native addon is available, agent identity fields are extracted via
446
+ * `cant_extract_agent_profiles` (cant-core, E8-AC2 / T11430). When the addon
447
+ * is absent the loader falls back to a minimal regex extractor so basic
448
+ * identity resolution still works in environments without the binary.
443
449
  *
444
450
  * @param agentsRoot - Optional override for the `packages/agents/` root. When
445
451
  * omitted the path is resolved automatically relative to this file. Tests
package/dist/parse.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { ParsedCANTMessage } from './types';
2
2
  export type { ParsedCANTMessage };
3
3
  /**
4
- * Initialize the CANT parser
4
+ * Initialize the CANT parser.
5
5
  *
6
- * With napi-rs native addons, this is a no-op (native modules load synchronously).
7
- * Kept for backward compatibility with code that previously called this for WASM init.
6
+ * With napi-rs native addons this is a no-op native modules load
7
+ * synchronously. Kept for backward compatibility with code that
8
+ * previously called this for WASM init.
8
9
  *
9
10
  * @example
10
11
  * ```typescript
@@ -16,13 +17,24 @@ export type { ParsedCANTMessage };
16
17
  */
17
18
  export declare function initCantParser(): Promise<void>;
18
19
  /**
19
- * Parse a CANT message
20
+ * Parse a CANT message via the canonical cant-core napi-rs path.
20
21
  *
21
- * If the native addon is available, uses the Rust cant-core parser via napi-rs.
22
- * Falls back to a basic JavaScript implementation if the native addon is not loaded.
22
+ * The native addon (Rust `cant-core`) is the single source of truth for
23
+ * CANT message parsing (E8-AC2: one canonical path). The previous
24
+ * JS regex fallback is intentionally NOT a routine code path — if the
25
+ * native addon is unavailable the function throws a typed error so
26
+ * callers know they are running in a degraded environment and cannot
27
+ * silently produce wrong results.
23
28
  *
24
- * @param content - The CANT message content to parse
25
- * @returns ParsedCANTMessage with directive, addresses, task_refs, tags
29
+ * In test environments where the binary is not present, mock
30
+ * `isNativeAvailable` or use the `CLEO_CANT_ALLOW_JS_FALLBACK=1`
31
+ * environment variable to enable the clearly-marked degraded-mode
32
+ * branch (emits a console.warn and returns a best-effort parse).
33
+ *
34
+ * @param content - The CANT message content to parse.
35
+ * @returns ParsedCANTMessage with directive, addresses, task_refs, tags.
36
+ * @throws {Error} When the native addon is unavailable and
37
+ * `CLEO_CANT_ALLOW_JS_FALLBACK` is not set.
26
38
  *
27
39
  * @example
28
40
  * ```typescript
package/dist/parse.js CHANGED
@@ -4,10 +4,11 @@ exports.initCantParser = initCantParser;
4
4
  exports.parseCANTMessage = parseCANTMessage;
5
5
  const native_loader_1 = require("./native-loader");
6
6
  /**
7
- * Initialize the CANT parser
7
+ * Initialize the CANT parser.
8
8
  *
9
- * With napi-rs native addons, this is a no-op (native modules load synchronously).
10
- * Kept for backward compatibility with code that previously called this for WASM init.
9
+ * With napi-rs native addons this is a no-op native modules load
10
+ * synchronously. Kept for backward compatibility with code that
11
+ * previously called this for WASM init.
11
12
  *
12
13
  * @example
13
14
  * ```typescript
@@ -22,13 +23,24 @@ async function initCantParser() {
22
23
  // Kept for backward compatibility.
23
24
  }
24
25
  /**
25
- * Parse a CANT message
26
+ * Parse a CANT message via the canonical cant-core napi-rs path.
26
27
  *
27
- * If the native addon is available, uses the Rust cant-core parser via napi-rs.
28
- * Falls back to a basic JavaScript implementation if the native addon is not loaded.
28
+ * The native addon (Rust `cant-core`) is the single source of truth for
29
+ * CANT message parsing (E8-AC2: one canonical path). The previous
30
+ * JS regex fallback is intentionally NOT a routine code path — if the
31
+ * native addon is unavailable the function throws a typed error so
32
+ * callers know they are running in a degraded environment and cannot
33
+ * silently produce wrong results.
29
34
  *
30
- * @param content - The CANT message content to parse
31
- * @returns ParsedCANTMessage with directive, addresses, task_refs, tags
35
+ * In test environments where the binary is not present, mock
36
+ * `isNativeAvailable` or use the `CLEO_CANT_ALLOW_JS_FALLBACK=1`
37
+ * environment variable to enable the clearly-marked degraded-mode
38
+ * branch (emits a console.warn and returns a best-effort parse).
39
+ *
40
+ * @param content - The CANT message content to parse.
41
+ * @returns ParsedCANTMessage with directive, addresses, task_refs, tags.
42
+ * @throws {Error} When the native addon is unavailable and
43
+ * `CLEO_CANT_ALLOW_JS_FALLBACK` is not set.
32
44
  *
33
45
  * @example
34
46
  * ```typescript
@@ -40,37 +52,47 @@ async function initCantParser() {
40
52
  * ```
41
53
  */
42
54
  function parseCANTMessage(content) {
43
- // If native addon is available, use it
55
+ // Canonical path: use the Rust cant-core parser via napi-rs.
44
56
  if ((0, native_loader_1.isNativeAvailable)()) {
45
- try {
46
- const nativeResult = (0, native_loader_1.cantParseNative)(content);
47
- return {
48
- directive: nativeResult.directive ?? undefined,
49
- directive_type: (nativeResult.directiveType?.toLowerCase() ??
50
- 'informational'),
51
- addresses: nativeResult.addresses ?? [],
52
- task_refs: nativeResult.taskRefs ?? [],
53
- tags: nativeResult.tags ?? [],
54
- header_raw: nativeResult.headerRaw ?? '',
55
- body: nativeResult.body ?? '',
56
- };
57
- }
58
- catch (error) {
59
- console.warn('Native parsing failed, falling back to JS:', error);
60
- }
57
+ const nativeResult = (0, native_loader_1.cantParseNative)(content);
58
+ return {
59
+ directive: nativeResult.directive ?? undefined,
60
+ directive_type: (nativeResult.directiveType?.toLowerCase() ??
61
+ 'informational'),
62
+ addresses: nativeResult.addresses ?? [],
63
+ task_refs: nativeResult.taskRefs ?? [],
64
+ tags: nativeResult.tags ?? [],
65
+ header_raw: nativeResult.headerRaw ?? '',
66
+ body: nativeResult.body ?? '',
67
+ };
68
+ }
69
+ // ── DEGRADED MODE ────────────────────────────────────────────────────────
70
+ // The native addon is NOT available. This is an explicit degraded-mode
71
+ // branch guarded by an environment variable so it is never activated
72
+ // silently in production (E8-AC2). Callers that need a best-effort parse
73
+ // in environments without the binary (CI matrix, edge runtimes) must
74
+ // set CLEO_CANT_ALLOW_JS_FALLBACK=1 explicitly.
75
+ if (process.env.CLEO_CANT_ALLOW_JS_FALLBACK !== '1') {
76
+ throw new Error('cant-core native addon not available. Build it with: cargo build --release -p cant-napi\n' +
77
+ 'Or set CLEO_CANT_ALLOW_JS_FALLBACK=1 to enable the degraded JS fallback parser.');
61
78
  }
62
- // Fallback: basic JS implementation (header/body split)
79
+ // Degraded JS fallback header/body split with basic regex.
80
+ // Only reached when CLEO_CANT_ALLOW_JS_FALLBACK=1.
81
+ console.warn('[cant] DEGRADED MODE: cant-core native addon unavailable; using JS regex parser. ' +
82
+ 'Results may differ from the canonical Rust parser. ' +
83
+ 'Build cant-napi to restore full fidelity.');
63
84
  const lines = content.split('\n');
64
- const header = lines[0] || '';
85
+ const header = lines[0] ?? '';
65
86
  const body = lines.slice(1).join('\n');
66
- // Basic regex extraction (not as robust as WASM parser)
67
87
  const directiveMatch = header.match(/^\/([a-z][a-z0-9-]*)/);
68
- const addresses = [...header.matchAll(/@([a-zA-Z][a-zA-Z0-9_-]*)/g)].map((m) => m[1]);
69
- const taskRefs = [...content.matchAll(/T(\d+)/g)].map((m) => `T${m[1]}`);
70
- const tags = [...content.matchAll(/#([a-zA-Z][a-zA-Z0-9_-]*)/g)].map((m) => m[1]);
88
+ const addresses = [...header.matchAll(/@([a-zA-Z][a-zA-Z0-9_-]*)/g)].map((m) => m[1] ?? '');
89
+ const taskRefs = [...content.matchAll(/T(\d+)/g)].map((m) => `T${m[1] ?? ''}`);
90
+ const tags = [...content.matchAll(/#([a-zA-Z][a-zA-Z0-9_-]*)/g)].map((m) => m[1] ?? '');
71
91
  return {
72
92
  directive: directiveMatch ? directiveMatch[1] : undefined,
73
- directive_type: directiveMatch ? classifyDirective(directiveMatch[1]) : 'informational',
93
+ directive_type: directiveMatch
94
+ ? _classifyDirectiveFallback(directiveMatch[1] ?? '')
95
+ : 'informational',
74
96
  addresses,
75
97
  task_refs: taskRefs,
76
98
  tags,
@@ -79,12 +101,13 @@ function parseCANTMessage(content) {
79
101
  };
80
102
  }
81
103
  /**
82
- * Classify a directive verb into its type
104
+ * Classify a directive verb into its type.
83
105
  *
106
+ * @internal Used only by the degraded JS fallback parser.
84
107
  * @param verb - The directive verb (e.g., 'done', 'action', 'info')
85
108
  * @returns 'actionable', 'routing', or 'informational'
86
109
  */
87
- function classifyDirective(verb) {
110
+ function _classifyDirectiveFallback(verb) {
88
111
  const actionable = ['claim', 'done', 'blocked', 'approve', 'decision', 'checkin'];
89
112
  const routing = ['action', 'review', 'proposal'];
90
113
  if (actionable.includes(verb))
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cleocode/cant",
3
- "version": "2026.5.133",
4
- "description": "CANT protocol parser and runtime for CLEO wraps cant-core via napi-rs",
3
+ "version": "2026.5.134",
4
+ "description": "CANT DSL cant-core (SSoT) parser, validator, and pipeline executor via napi-rs; agent identity, message parsing, and migration utilities",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
@@ -9,9 +9,9 @@
9
9
  "napi/"
10
10
  ],
11
11
  "dependencies": {
12
- "@cleocode/contracts": "2026.5.133",
13
- "@cleocode/core": "2026.5.133",
14
- "@cleocode/lafs": "2026.5.133"
12
+ "@cleocode/contracts": "2026.5.134",
13
+ "@cleocode/core": "2026.5.134",
14
+ "@cleocode/lafs": "2026.5.134"
15
15
  },
16
16
  "devDependencies": {
17
17
  "typescript": "^6.0.2",