@cleocode/cant 2026.9.18 → 2026.9.20

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
@@ -140,15 +140,38 @@ pnpm --filter @cleocode/cant test
140
140
  Tests use real `.cant` fixtures from `crates/cant-core/fixtures/` and
141
141
  seed agents from `packages/agents/seed-agents/`.
142
142
 
143
- ## Rebuilding the napi binary
143
+ ## Native binaries and the WebAssembly fallback
144
+
145
+ The published package bundles the cant-napi addon for every supported
146
+ platform in `napi/` (T12382): native binaries for linux x64/arm64 (glibc and
147
+ musl), darwin x64/arm64 and win32 x64/arm64, plus
148
+ `cant.wasm32-wasi.wasm`, the same crate built for `wasm32-wasip1-threads`.
149
+ The napi-rs generated loader (`napi/index.cjs`) uses the native binary for
150
+ the host and falls back to the WebAssembly build automatically when none
151
+ matches. `NAPI_RS_FORCE_WASI=error` forces the WebAssembly build (tests use
152
+ it to prove the fallback). `cantAddonBackend()` reports which one loaded.
153
+
154
+ Parsing, validation and profile extraction are identical on both backends
155
+ (`tests/native-wasi-parity.test.ts`). One function differs:
156
+ `cantExecutePipelineNative` needs the native backend, because pipelines spawn
157
+ subprocesses through cant-runtime's multi-thread tokio runtime, which WASI
158
+ cannot provide. Under WASI it resolves to `success: false` with an `error`
159
+ saying so; it does not throw. Node prints a one-time
160
+ `ExperimentalWarning: WASI is an experimental feature` to stderr when the
161
+ WebAssembly build loads.
162
+
163
+ To build locally (Rust toolchain required; nothing in `napi/` is committed):
144
164
 
145
165
  ```bash
146
- pnpm --filter @cleocode/cant build:napi
166
+ pnpm --filter @cleocode/cant build:napi # host native binary + loader
167
+ # WebAssembly build: needs `rustup target add wasm32-wasip1-threads`, and
168
+ # RUSTC must be a full path so napi-build can find crt1-reactor.o.
169
+ RUSTC="$(rustup which rustc)" pnpm --filter @cleocode/cant build:napi:wasi
147
170
  ```
148
171
 
149
- Requires a Rust toolchain. Produces `napi/cant.linux-x64-gnu.node`.
150
- Other platform triples are added via the workspace release pipeline,
151
- not locally.
172
+ CI builds all 9 artifacts in `.github/workflows/cant-napi-build.yml`, which
173
+ the release calls; the release fails unless every triple is packed and
174
+ stamped with the released commit.
152
175
 
153
176
  ## Crate track
154
177
 
package/dist/index.d.ts CHANGED
@@ -12,8 +12,8 @@ export type { ConsolidateOptions, MentalModel, MentalModelObservation, MentalMod
12
12
  export { consolidate, createEmptyModel, harvestObservations, renderMentalModel, } from './mental-model.js';
13
13
  export type { ConvertedFile, MigrationOptions, MigrationResult, UnconvertedSection, } from './migrate/index';
14
14
  export { migrateMarkdown, serializeCantDocument, showDiff, showSummary } from './migrate/index';
15
- export type { AgentProfile, NativeDiagnostic, NativeParseDocumentResult, NativeParseError, NativeParseResult, NativePipelineResult, NativePipelineStep, NativeValidateResult, SeedPersonaId, } from './native-loader';
16
- export { cantClassifyDirectiveNative, cantExecutePipelineNative, cantExtractAgentProfilesNative, cantParseDocumentNative, cantParseNative, cantValidateDocumentNative, extractAgentProfilesTyped, extractAgentSkills, initWasm, isNativeAvailable, isWasmAvailable, loadSeedAgentIdentities, SEED_PERSONA_IDS, validateAgentCantPath, } from './native-loader';
15
+ export type { AgentProfile, CantAddonBackend, NativeDiagnostic, NativeParseDocumentResult, NativeParseError, NativeParseResult, NativePipelineResult, NativePipelineStep, NativeValidateResult, SeedPersonaId, } from './native-loader';
16
+ export { cantAddonBackend, cantAddonBuildInfo, cantClassifyDirectiveNative, cantExecutePipelineNative, cantExtractAgentProfilesNative, cantParseDocumentNative, cantParseNative, cantValidateDocumentNative, extractAgentProfilesTyped, extractAgentSkills, initWasm, isNativeAvailable, isWasmAvailable, loadSeedAgentIdentities, SEED_PERSONA_IDS, validateAgentCantPath, } from './native-loader';
17
17
  export type { ParsedCANTMessage } from './parse';
18
18
  export { initCantParser, parseCANTMessage } from './parse';
19
19
  export type { CantAgentV3, CantContextSourceDef, CantContractBlock, CantContractClause, CantMentalModelRef, CantOverflowStrategy, CantPathPermissions, CantTier, DirectiveType, } from './types';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // JIT Agent Composer (ULTRAPLAN Wave 5)
3
3
  // Wave 7a: BRAIN-backed ContextProvider (T432)
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.isCantAgentV3 = exports.parseCANTMessage = exports.initCantParser = exports.validateAgentCantPath = exports.SEED_PERSONA_IDS = exports.loadSeedAgentIdentities = exports.isWasmAvailable = exports.isNativeAvailable = exports.initWasm = exports.extractAgentSkills = exports.extractAgentProfilesTyped = exports.cantValidateDocumentNative = exports.cantParseNative = exports.cantParseDocumentNative = exports.cantExtractAgentProfilesNative = exports.cantExecutePipelineNative = exports.cantClassifyDirectiveNative = exports.showSummary = exports.showDiff = exports.serializeCantDocument = exports.migrateMarkdown = exports.renderMentalModel = exports.harvestObservations = exports.createEmptyModel = exports.consolidate = exports.WORKER_FORBIDDEN_SPAWN_TOOLS = exports.validateSpawnRequest = exports.THIN_AGENT_TOOLS_STRIPPED = exports.stripSpawnToolsForWorker = exports.ORCHESTRATOR_FORBIDDEN_TOOLS = exports.LEAD_FORBIDDEN_TOOLS = exports.filterToolsForRole = exports.validateDocument = exports.parseDocument = exports.listSections = exports.executePipeline = exports.brainContextProvider = exports.TIER_CAPS = exports.estimateTokens = exports.escalateTier = exports.composeSpawnPayload = exports.toCantAgentV3 = exports.compileBundle = void 0;
5
+ exports.isCantAgentV3 = exports.parseCANTMessage = exports.initCantParser = exports.validateAgentCantPath = exports.SEED_PERSONA_IDS = exports.loadSeedAgentIdentities = exports.isWasmAvailable = exports.isNativeAvailable = exports.initWasm = exports.extractAgentSkills = exports.extractAgentProfilesTyped = exports.cantValidateDocumentNative = exports.cantParseNative = exports.cantParseDocumentNative = exports.cantExtractAgentProfilesNative = exports.cantExecutePipelineNative = exports.cantClassifyDirectiveNative = exports.cantAddonBuildInfo = exports.cantAddonBackend = exports.showSummary = exports.showDiff = exports.serializeCantDocument = exports.migrateMarkdown = exports.renderMentalModel = exports.harvestObservations = exports.createEmptyModel = exports.consolidate = exports.WORKER_FORBIDDEN_SPAWN_TOOLS = exports.validateSpawnRequest = exports.THIN_AGENT_TOOLS_STRIPPED = exports.stripSpawnToolsForWorker = exports.ORCHESTRATOR_FORBIDDEN_TOOLS = exports.LEAD_FORBIDDEN_TOOLS = exports.filterToolsForRole = exports.validateDocument = exports.parseDocument = exports.listSections = exports.executePipeline = exports.brainContextProvider = exports.TIER_CAPS = exports.estimateTokens = exports.escalateTier = exports.composeSpawnPayload = exports.toCantAgentV3 = exports.compileBundle = void 0;
6
6
  // Bundle compiler
7
7
  var bundle_1 = require("./bundle");
8
8
  Object.defineProperty(exports, "compileBundle", { enumerable: true, get: function () { return bundle_1.compileBundle; } });
@@ -43,6 +43,8 @@ Object.defineProperty(exports, "showDiff", { enumerable: true, get: function ()
43
43
  Object.defineProperty(exports, "showSummary", { enumerable: true, get: function () { return index_1.showSummary; } });
44
44
  // Native loader (replaces wasm-loader)
45
45
  var native_loader_1 = require("./native-loader");
46
+ Object.defineProperty(exports, "cantAddonBackend", { enumerable: true, get: function () { return native_loader_1.cantAddonBackend; } });
47
+ Object.defineProperty(exports, "cantAddonBuildInfo", { enumerable: true, get: function () { return native_loader_1.cantAddonBuildInfo; } });
46
48
  Object.defineProperty(exports, "cantClassifyDirectiveNative", { enumerable: true, get: function () { return native_loader_1.cantClassifyDirectiveNative; } });
47
49
  Object.defineProperty(exports, "cantExecutePipelineNative", { enumerable: true, get: function () { return native_loader_1.cantExecutePipelineNative; } });
48
50
  Object.defineProperty(exports, "cantExtractAgentProfilesNative", { enumerable: true, get: function () { return native_loader_1.cantExtractAgentProfilesNative; } });
@@ -1,14 +1,24 @@
1
1
  /**
2
- * Native addon loader for cant-core via napi-rs.
2
+ * Addon loader for cant-core via napi-rs.
3
3
  *
4
4
  * @remarks
5
- * Loads the napi-rs native addon synchronously on first use. Tries the
6
- * package-local binary first (`packages/cant/napi/cant.linux-x64-gnu.node`),
7
- * then falls back to the workspace `cant-napi` crate's `index.cjs` for
8
- * dev-mode builds where the package binary may not be present yet.
9
- *
10
- * Replaces the previous WASM loader. Follows the same pattern as
11
- * `packages/lafs/src/native-loader.ts`.
5
+ * Loads the cant-napi addon synchronously on first use through the napi-rs
6
+ * GENERATED loader shipped as `napi/index.cjs` (T12382). That loader tries a
7
+ * native binary for this OS/CPU first (`napi/cant.<triple>.node`, built for
8
+ * linux x64/arm64 gnu+musl, darwin x64/arm64 and win32 x64/arm64) and falls
9
+ * back automatically to the WebAssembly build of the same crate
10
+ * (`napi/cant.wasm32-wasi.wasm`, target `wasm32-wasip1-threads`) when no
11
+ * native binary matches. `NAPI_RS_FORCE_WASI=error` forces the WASI path.
12
+ *
13
+ * Both backends compile the same Rust source, so parse/validate/extract
14
+ * results are identical (proven by `tests/native-wasi-parity.test.ts`). The
15
+ * one difference is {@link cantExecutePipelineNative}: pipelines spawn
16
+ * subprocesses through cant-runtime's multi-thread tokio runtime, which WASI
17
+ * cannot provide, so under WASI it resolves to `success: false` with an
18
+ * `error` explaining why instead of running.
19
+ *
20
+ * In a source checkout, build the addon with
21
+ * `pnpm --filter @cleocode/cant build:napi` (and `build:napi:wasi`).
12
22
  */
13
23
  import type { PeerIdentity } from '@cleocode/contracts';
14
24
  /** Shape of a parsed CANT message returned by the native binding. */
@@ -107,11 +117,45 @@ export interface NativePipelineResult {
107
117
  error?: string | null;
108
118
  }
109
119
  /**
110
- * Check if the native addon is available.
120
+ * Which build of the cant-napi addon is serving calls.
121
+ *
122
+ * - `native` — a per-OS/CPU `.node` binary.
123
+ * - `wasi` — the `wasm32-wasip1-threads` WebAssembly fallback.
124
+ */
125
+ export type CantAddonBackend = 'native' | 'wasi';
126
+ /**
127
+ * Check if the cant-napi addon (native or WebAssembly) is available.
128
+ *
129
+ * @remarks
130
+ * The name predates the WASI fallback and is kept for compatibility: it is
131
+ * `true` when EITHER backend loaded. Use {@link cantAddonBackend} to tell
132
+ * which one.
111
133
  *
112
- * @returns `true` if the native Rust binding loaded successfully.
134
+ * @returns `true` if the Rust binding loaded successfully.
113
135
  */
114
136
  export declare function isNativeAvailable(): boolean;
137
+ /**
138
+ * Report which backend of the cant-napi addon loaded.
139
+ *
140
+ * @returns `'native'` or `'wasi'`, or `null` when neither could be loaded.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import { cantAddonBackend } from '@cleocode/cant';
145
+ *
146
+ * if (cantAddonBackend() === 'wasi') {
147
+ * // pipelines cannot run here; parsing and validation still work
148
+ * }
149
+ * ```
150
+ */
151
+ export declare function cantAddonBackend(): CantAddonBackend | null;
152
+ /**
153
+ * Return the source-revision stamp compiled into the loaded addon, e.g.
154
+ * `"cant-napi-source-rev:<git sha>"` (`"…:unversioned"` for local builds).
155
+ *
156
+ * @returns The stamp, or `null` when the addon could not be loaded.
157
+ */
158
+ export declare function cantAddonBuildInfo(): string | null;
115
159
  /**
116
160
  * Parse a CANT message using the native addon.
117
161
  *
@@ -226,6 +270,12 @@ export declare function validateAgentCantPath(cantPath: string, rootDir: string)
226
270
  /**
227
271
  * Execute a deterministic pipeline from a `.cant` file via the native addon.
228
272
  *
273
+ * @remarks
274
+ * Native backend only. Under the WebAssembly (WASI) fallback this resolves
275
+ * to `success: false` with an `error` explaining that pipelines need a native
276
+ * binary: they spawn subprocesses through cant-runtime's multi-thread tokio
277
+ * runtime, which WASI cannot provide. It never throws for that case.
278
+ *
229
279
  * @param filePath - Absolute or relative path to a `.cant` file.
230
280
  * @param pipelineName - The name of the `pipeline { ... }` block to run.
231
281
  */
@@ -1,19 +1,31 @@
1
1
  "use strict";
2
2
  /**
3
- * Native addon loader for cant-core via napi-rs.
3
+ * Addon loader for cant-core via napi-rs.
4
4
  *
5
5
  * @remarks
6
- * Loads the napi-rs native addon synchronously on first use. Tries the
7
- * package-local binary first (`packages/cant/napi/cant.linux-x64-gnu.node`),
8
- * then falls back to the workspace `cant-napi` crate's `index.cjs` for
9
- * dev-mode builds where the package binary may not be present yet.
10
- *
11
- * Replaces the previous WASM loader. Follows the same pattern as
12
- * `packages/lafs/src/native-loader.ts`.
6
+ * Loads the cant-napi addon synchronously on first use through the napi-rs
7
+ * GENERATED loader shipped as `napi/index.cjs` (T12382). That loader tries a
8
+ * native binary for this OS/CPU first (`napi/cant.<triple>.node`, built for
9
+ * linux x64/arm64 gnu+musl, darwin x64/arm64 and win32 x64/arm64) and falls
10
+ * back automatically to the WebAssembly build of the same crate
11
+ * (`napi/cant.wasm32-wasi.wasm`, target `wasm32-wasip1-threads`) when no
12
+ * native binary matches. `NAPI_RS_FORCE_WASI=error` forces the WASI path.
13
+ *
14
+ * Both backends compile the same Rust source, so parse/validate/extract
15
+ * results are identical (proven by `tests/native-wasi-parity.test.ts`). The
16
+ * one difference is {@link cantExecutePipelineNative}: pipelines spawn
17
+ * subprocesses through cant-runtime's multi-thread tokio runtime, which WASI
18
+ * cannot provide, so under WASI it resolves to `success: false` with an
19
+ * `error` explaining why instead of running.
20
+ *
21
+ * In a source checkout, build the addon with
22
+ * `pnpm --filter @cleocode/cant build:napi` (and `build:napi:wasi`).
13
23
  */
14
24
  Object.defineProperty(exports, "__esModule", { value: true });
15
25
  exports.initWasm = exports.isWasmAvailable = exports.SEED_PERSONA_IDS = void 0;
16
26
  exports.isNativeAvailable = isNativeAvailable;
27
+ exports.cantAddonBackend = cantAddonBackend;
28
+ exports.cantAddonBuildInfo = cantAddonBuildInfo;
17
29
  exports.cantParseNative = cantParseNative;
18
30
  exports.cantClassifyDirectiveNative = cantClassifyDirectiveNative;
19
31
  exports.cantParseDocumentNative = cantParseDocumentNative;
@@ -29,65 +41,83 @@ const node_fs_1 = require("node:fs");
29
41
  const node_path_1 = require("node:path");
30
42
  let nativeModule = null;
31
43
  let loadAttempted = false;
44
+ let loadError = null;
32
45
  /**
33
- * Attempt to load the native addon. Called lazily on first use.
34
- * Native addons load synchronously via require() — no async init needed.
46
+ * Attempt to load the addon. Called lazily on first use.
47
+ * The generated loader is synchronous (native `require()`, or a synchronous
48
+ * WASI instantiation), so no async init is needed.
35
49
  */
36
50
  function ensureLoaded() {
37
51
  if (loadAttempted)
38
52
  return;
39
53
  loadAttempted = true;
40
- // The compiled file lives at packages/cant/dist/native-loader.js, so
41
- // ../napi resolves to packages/cant/napi/cant.<platform>.node.
42
- const packageBinary = (0, node_path_1.join)(__dirname, '..', 'napi', `cant.${nativePlatformTriple()}.node`);
43
- try {
44
- nativeModule = require(packageBinary);
45
- return;
46
- }
47
- catch {
48
- // Fall through to workspace dev fallback.
49
- }
54
+ // Both packages/cant/src/ (tests) and packages/cant/dist/ (built, installed)
55
+ // sit next to packages/cant/napi/, which holds the generated loader.
50
56
  try {
51
- // Development fallback: load via the cant-napi crate's index.cjs.
52
- // From packages/cant/dist/ -> ../../../crates/cant-napi/index.cjs.
53
- nativeModule = require('../../../crates/cant-napi/index.cjs');
57
+ const binding = require((0, node_path_1.join)(__dirname, '..', 'napi', 'index.cjs'));
58
+ nativeModule = binding;
54
59
  }
55
- catch {
60
+ catch (err) {
56
61
  nativeModule = null;
62
+ loadError = err instanceof Error ? err.message : String(err);
57
63
  }
58
64
  }
59
- /** Resolve the package-local napi-rs binary suffix for the current platform. */
60
- function nativePlatformTriple() {
61
- if (process.platform === 'darwin') {
62
- return process.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64';
63
- }
64
- if (process.platform === 'linux') {
65
- return process.arch === 'arm64' ? 'linux-arm64-gnu' : 'linux-x64-gnu';
66
- }
67
- if (process.platform === 'win32') {
68
- return process.arch === 'arm64' ? 'win32-arm64-msvc' : 'win32-x64-msvc';
69
- }
70
- return `${process.platform}-${process.arch}`;
71
- }
72
65
  /**
73
- * Check if the native addon is available.
66
+ * Check if the cant-napi addon (native or WebAssembly) is available.
67
+ *
68
+ * @remarks
69
+ * The name predates the WASI fallback and is kept for compatibility: it is
70
+ * `true` when EITHER backend loaded. Use {@link cantAddonBackend} to tell
71
+ * which one.
74
72
  *
75
- * @returns `true` if the native Rust binding loaded successfully.
73
+ * @returns `true` if the Rust binding loaded successfully.
76
74
  */
77
75
  function isNativeAvailable() {
78
76
  ensureLoaded();
79
77
  return nativeModule !== null;
80
78
  }
81
79
  /**
82
- * Get the native module, throwing if it failed to load.
80
+ * Report which backend of the cant-napi addon loaded.
81
+ *
82
+ * @returns `'native'` or `'wasi'`, or `null` when neither could be loaded.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { cantAddonBackend } from '@cleocode/cant';
87
+ *
88
+ * if (cantAddonBackend() === 'wasi') {
89
+ * // pipelines cannot run here; parsing and validation still work
90
+ * }
91
+ * ```
92
+ */
93
+ function cantAddonBackend() {
94
+ ensureLoaded();
95
+ if (!nativeModule)
96
+ return null;
97
+ return nativeModule.cantBackend() === 'wasi' ? 'wasi' : 'native';
98
+ }
99
+ /**
100
+ * Return the source-revision stamp compiled into the loaded addon, e.g.
101
+ * `"cant-napi-source-rev:<git sha>"` (`"…:unversioned"` for local builds).
102
+ *
103
+ * @returns The stamp, or `null` when the addon could not be loaded.
104
+ */
105
+ function cantAddonBuildInfo() {
106
+ ensureLoaded();
107
+ return nativeModule ? nativeModule.cantBuildInfo() : null;
108
+ }
109
+ /**
110
+ * Get the addon module, throwing if it failed to load.
83
111
  *
84
112
  * @internal
85
- * @throws Error when the native addon could not be loaded.
113
+ * @throws Error when neither a native binary nor the WebAssembly fallback loaded.
86
114
  */
87
115
  function requireNative() {
88
116
  ensureLoaded();
89
117
  if (!nativeModule) {
90
- throw new Error('cant-napi native addon not available. Build it with: cargo build --release -p cant-napi');
118
+ throw new Error(`CANT parser addon not available: no native binary for ${process.platform}-${process.arch} ` +
119
+ `loaded and the WebAssembly fallback did not load either (${loadError ?? 'no load error was recorded'}). ` +
120
+ 'Reinstall @cleocode/cant; in a source checkout run: pnpm --filter @cleocode/cant build:napi');
91
121
  }
92
122
  return nativeModule;
93
123
  }
@@ -236,10 +266,16 @@ function validateAgentCantPath(cantPath, rootDir) {
236
266
  /**
237
267
  * Execute a deterministic pipeline from a `.cant` file via the native addon.
238
268
  *
269
+ * @remarks
270
+ * Native backend only. Under the WebAssembly (WASI) fallback this resolves
271
+ * to `success: false` with an `error` explaining that pipelines need a native
272
+ * binary: they spawn subprocesses through cant-runtime's multi-thread tokio
273
+ * runtime, which WASI cannot provide. It never throws for that case.
274
+ *
239
275
  * @param filePath - Absolute or relative path to a `.cant` file.
240
276
  * @param pipelineName - The name of the `pipeline { ... }` block to run.
241
277
  */
242
- function cantExecutePipelineNative(filePath, pipelineName) {
278
+ async function cantExecutePipelineNative(filePath, pipelineName) {
243
279
  return requireNative().cantExecutePipeline(filePath, pipelineName);
244
280
  }
245
281
  // ============================================================================
package/dist/parse.js CHANGED
@@ -73,7 +73,8 @@ function parseCANTMessage(content) {
73
73
  // in environments without the binary (CI matrix, edge runtimes) must
74
74
  // set CLEO_CANT_ALLOW_JS_FALLBACK=1 explicitly.
75
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' +
76
+ throw new Error('cant-core addon not available (no native binary and no WebAssembly fallback). Reinstall ' +
77
+ '@cleocode/cant; in a source checkout run: pnpm --filter @cleocode/cant build:napi\n' +
77
78
  'Or set CLEO_CANT_ALLOW_JS_FALLBACK=1 to enable the degraded JS fallback parser.');
78
79
  }
79
80
  // Degraded JS fallback — header/body split with basic regex.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file