@cleocode/cant 2026.4.15 → 2026.4.17

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.
@@ -102,6 +102,50 @@ export interface SpawnPayload {
102
102
  /** Whether mental model was injected. */
103
103
  mentalModelInjected: boolean;
104
104
  }
105
+ /**
106
+ * Path-scoped file permissions for a CANT agent (T423 — ULTRAPLAN §9.2).
107
+ *
108
+ * Mirrors the Rust `PathPermissions` struct in `crates/cant-core/src/dsl/ast.rs`.
109
+ * Enforced at runtime by the `tool_call` hook in `cleo-cant-bridge.ts`.
110
+ *
111
+ * Security rules:
112
+ * - An **empty `write` array means no writes are allowed** (default-deny).
113
+ * - An absent field (`undefined`) means that access level is unrestricted.
114
+ *
115
+ * @example
116
+ * ```cant
117
+ * agent backend-dev:
118
+ * permissions:
119
+ * files:
120
+ * write: ["packages/cleo/**", "crates/**"]
121
+ * read: ["**\/*"]
122
+ * delete: ["packages/cleo/**"]
123
+ * ```
124
+ *
125
+ * @task T423
126
+ */
127
+ export interface PathPermissions {
128
+ /**
129
+ * Glob patterns for paths this agent may write (Edit/Write tool).
130
+ *
131
+ * An empty array means no writes are allowed (read-only agent).
132
+ * `undefined` means no write ACL was declared (unrestricted).
133
+ */
134
+ write?: string[];
135
+ /**
136
+ * Glob patterns for paths this agent may read.
137
+ *
138
+ * `undefined` means reads are unrestricted (default-allow).
139
+ */
140
+ read?: string[];
141
+ /**
142
+ * Glob patterns for paths this agent may delete.
143
+ *
144
+ * An empty array means no deletes are allowed.
145
+ * `undefined` means deletes are unrestricted.
146
+ */
147
+ delete?: string[];
148
+ }
105
149
  /** Agent definition extracted from a compiled `.cant` bundle. */
106
150
  export interface AgentDefinition {
107
151
  /** The agent name as declared in the `.cant` file. */
@@ -143,6 +187,15 @@ export interface AgentDefinition {
143
187
  } | null;
144
188
  /** Behavior when total tokens exceed the tier cap. */
145
189
  onOverflow: 'escalate_tier' | 'fail';
190
+ /**
191
+ * Path-scoped file permissions declared via `permissions: files:` in the `.cant` file.
192
+ *
193
+ * `undefined` means no path ACL was declared (tool-level enforcement only).
194
+ * When present, the `tool_call` hook enforces write/delete path restrictions.
195
+ *
196
+ * @task T423
197
+ */
198
+ filePermissions?: PathPermissions;
146
199
  }
147
200
  /**
148
201
  * Compose a spawn payload for an agent.
@@ -0,0 +1,47 @@
1
+ /**
2
+ * BRAIN-backed ContextProvider implementation for the JIT Agent Composer.
3
+ *
4
+ * Wires the `ContextProvider` interface from `composer.ts` to the live BRAIN
5
+ * database via `memoryFind` from `@cleocode/core`. This is the critical T432
6
+ * activation — without this provider, `composeSpawnPayload` cannot pull real
7
+ * context from BRAIN at spawn time.
8
+ *
9
+ * The `agent` filter (T417/T418) ensures that mental model observations written
10
+ * by a specific agent are returned rather than unrelated global observations.
11
+ *
12
+ * @epic T377
13
+ * @task T432
14
+ * @packageDocumentation
15
+ */
16
+ import type { ContextProvider } from './composer.js';
17
+ /**
18
+ * A {@link ContextProvider} implementation that retrieves context from the
19
+ * BRAIN database (`brain.db`) via the `memoryFind` and `memoryFetch` functions
20
+ * from `@cleocode/core`.
21
+ *
22
+ * @remarks
23
+ * This provider is designed for use in the canonical spawn path:
24
+ * `composeSpawnPayload(agentDef, brainContextProvider(projectRoot), projectHash)`.
25
+ *
26
+ * The `source` parameter maps to BRAIN table categories:
27
+ * - `"patterns"` → searches the patterns table
28
+ * - `"decisions"` → searches the decisions table
29
+ * - `"learnings"` → searches the learnings table
30
+ * - `"observations"` → searches the observations table (default)
31
+ *
32
+ * @param projectRoot - Absolute path to the project root (where `.cleo/` lives).
33
+ * @returns A `ContextProvider` instance backed by the project's BRAIN database.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * import { composeSpawnPayload } from '@cleocode/cant';
38
+ * import { brainContextProvider } from '@cleocode/cant/context-provider-brain';
39
+ *
40
+ * const payload = await composeSpawnPayload(
41
+ * agentDef,
42
+ * brainContextProvider('/project/root'),
43
+ * projectHash,
44
+ * );
45
+ * ```
46
+ */
47
+ export declare function brainContextProvider(projectRoot: string): ContextProvider;
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+ /**
3
+ * BRAIN-backed ContextProvider implementation for the JIT Agent Composer.
4
+ *
5
+ * Wires the `ContextProvider` interface from `composer.ts` to the live BRAIN
6
+ * database via `memoryFind` from `@cleocode/core`. This is the critical T432
7
+ * activation — without this provider, `composeSpawnPayload` cannot pull real
8
+ * context from BRAIN at spawn time.
9
+ *
10
+ * The `agent` filter (T417/T418) ensures that mental model observations written
11
+ * by a specific agent are returned rather than unrelated global observations.
12
+ *
13
+ * @epic T377
14
+ * @task T432
15
+ * @packageDocumentation
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.brainContextProvider = brainContextProvider;
19
+ const composer_js_1 = require("./composer.js");
20
+ /** Lazy-load memoryFind and memoryFetch from @cleocode/core/internal. */
21
+ async function loadBrainOps() {
22
+ // Variable path prevents tsc from resolving the module at type-check time.
23
+ const modPath = '@cleocode/core/internal';
24
+ const mod = await import(/* webpackIgnore: true */ modPath);
25
+ return { memoryFind: mod.memoryFind, memoryFetch: mod.memoryFetch };
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Internal helpers
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Truncate text to fit within a token budget using a 4 chars/token estimate.
32
+ *
33
+ * @param text - The text to truncate.
34
+ * @param maxTokens - The maximum token count.
35
+ * @returns The (possibly truncated) text.
36
+ */
37
+ function truncateToTokenBudget(text, maxTokens) {
38
+ if (maxTokens <= 0)
39
+ return '';
40
+ const maxChars = maxTokens * 4;
41
+ if (text.length <= maxChars)
42
+ return text;
43
+ return text.slice(0, maxChars);
44
+ }
45
+ // ---------------------------------------------------------------------------
46
+ // BRAIN ContextProvider
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * A {@link ContextProvider} implementation that retrieves context from the
50
+ * BRAIN database (`brain.db`) via the `memoryFind` and `memoryFetch` functions
51
+ * from `@cleocode/core`.
52
+ *
53
+ * @remarks
54
+ * This provider is designed for use in the canonical spawn path:
55
+ * `composeSpawnPayload(agentDef, brainContextProvider(projectRoot), projectHash)`.
56
+ *
57
+ * The `source` parameter maps to BRAIN table categories:
58
+ * - `"patterns"` → searches the patterns table
59
+ * - `"decisions"` → searches the decisions table
60
+ * - `"learnings"` → searches the learnings table
61
+ * - `"observations"` → searches the observations table (default)
62
+ *
63
+ * @param projectRoot - Absolute path to the project root (where `.cleo/` lives).
64
+ * @returns A `ContextProvider` instance backed by the project's BRAIN database.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { composeSpawnPayload } from '@cleocode/cant';
69
+ * import { brainContextProvider } from '@cleocode/cant/context-provider-brain';
70
+ *
71
+ * const payload = await composeSpawnPayload(
72
+ * agentDef,
73
+ * brainContextProvider('/project/root'),
74
+ * projectHash,
75
+ * );
76
+ * ```
77
+ */
78
+ function brainContextProvider(projectRoot) {
79
+ return {
80
+ /**
81
+ * Query BRAIN for context entries matching a source category and query string.
82
+ *
83
+ * @param source - BRAIN table category (patterns, decisions, learnings, observations).
84
+ * @param query - The search query string.
85
+ * @param maxTokens - Maximum token budget for the returned content.
86
+ * @returns A ContextSlice with content truncated to the token budget.
87
+ */
88
+ async queryContext(source, query, maxTokens) {
89
+ if (maxTokens <= 0) {
90
+ return { source, content: '', tokens: 0 };
91
+ }
92
+ try {
93
+ // Map source to a BRAIN table filter.
94
+ const validTables = ['patterns', 'decisions', 'learnings', 'observations'];
95
+ const table = validTables.includes(source)
96
+ ? [source]
97
+ : undefined;
98
+ const { memoryFind, memoryFetch } = await loadBrainOps();
99
+ const findResult = await memoryFind({
100
+ query,
101
+ limit: 10,
102
+ tables: table,
103
+ }, projectRoot);
104
+ if (!findResult.success || !findResult.data) {
105
+ return { source, content: '', tokens: 0 };
106
+ }
107
+ // memoryFind returns { results: BrainCompactHit[] }
108
+ const data = findResult.data;
109
+ const hits = data.results ?? [];
110
+ if (hits.length === 0) {
111
+ return { source, content: '', tokens: 0 };
112
+ }
113
+ // Fetch full content for the top hits within the budget.
114
+ const ids = hits.map((h) => h.id);
115
+ const fetchResult = await memoryFetch({ ids }, projectRoot);
116
+ let content = '';
117
+ if (fetchResult.success && fetchResult.data) {
118
+ const fetchData = fetchResult.data;
119
+ const entries = fetchData.entries ?? [];
120
+ content = entries.map((e) => `[${e.title}]\n${e.content ?? e.text ?? ''}`).join('\n\n');
121
+ }
122
+ else {
123
+ // Fallback to compact titles if fetch failed.
124
+ content = hits.map((h) => h.title).join('\n');
125
+ }
126
+ const truncated = truncateToTokenBudget(content, maxTokens);
127
+ return {
128
+ source,
129
+ content: truncated,
130
+ tokens: (0, composer_js_1.estimateTokens)(truncated),
131
+ };
132
+ }
133
+ catch {
134
+ // Return empty slice on any error — the composer handles missing context gracefully.
135
+ return { source, content: '', tokens: 0 };
136
+ }
137
+ },
138
+ /**
139
+ * Load the mental model for an agent from BRAIN observations.
140
+ *
141
+ * Uses the T417/T418 `agent` filter to retrieve only observations
142
+ * produced by the specific agent for this project scope.
143
+ *
144
+ * @param agentName - The agent name to load the mental model for.
145
+ * @param projectHash - Project identifier ('global' for global scope).
146
+ * @param maxTokens - Maximum token budget for the mental model content.
147
+ * @returns A MentalModelSlice with content truncated to the token budget.
148
+ */
149
+ async loadMentalModel(agentName, projectHash, maxTokens) {
150
+ if (maxTokens <= 0) {
151
+ return { content: '', tokens: 0, lastConsolidated: null };
152
+ }
153
+ try {
154
+ const { memoryFind, memoryFetch } = await loadBrainOps();
155
+ // Query brain_observations filtered by agent name (T417/T418).
156
+ const findResult = await memoryFind({
157
+ query: `mental model ${agentName}`,
158
+ limit: 5,
159
+ tables: ['observations'],
160
+ agent: agentName,
161
+ }, projectRoot);
162
+ if (!findResult.success || !findResult.data) {
163
+ return { content: '', tokens: 0, lastConsolidated: null };
164
+ }
165
+ const data = findResult.data;
166
+ const hits = data.results ?? [];
167
+ if (hits.length === 0) {
168
+ return { content: '', tokens: 0, lastConsolidated: null };
169
+ }
170
+ // Sort by date descending and take the most recent entries.
171
+ const sorted = [...hits].sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
172
+ const ids = sorted.map((h) => h.id);
173
+ const fetchResult = await memoryFetch({ ids }, projectRoot);
174
+ let content = '';
175
+ let lastConsolidated = null;
176
+ if (fetchResult.success && fetchResult.data) {
177
+ const fetchData = fetchResult.data;
178
+ const entries = fetchData.entries ?? [];
179
+ content = entries.map((e) => `[${e.title}]\n${e.content ?? e.text ?? ''}`).join('\n\n');
180
+ lastConsolidated = entries[0]?.date ?? null;
181
+ }
182
+ else {
183
+ content = sorted.map((h) => h.title).join('\n');
184
+ lastConsolidated = sorted[0]?.date ?? null;
185
+ }
186
+ // Scope prefix to help the agent understand its mental model context.
187
+ const prefix = projectHash === 'global'
188
+ ? `[Global mental model for ${agentName}]\n`
189
+ : `[Project mental model for ${agentName} — scope: ${projectHash}]\n`;
190
+ const full = prefix + content;
191
+ const truncated = truncateToTokenBudget(full, maxTokens);
192
+ return {
193
+ content: truncated,
194
+ tokens: (0, composer_js_1.estimateTokens)(truncated),
195
+ lastConsolidated,
196
+ };
197
+ }
198
+ catch {
199
+ return { content: '', tokens: 0, lastConsolidated: null };
200
+ }
201
+ },
202
+ };
203
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  export type { LAFSEnvelope, LAFSError, LAFSMeta, MVILevel } from '@cleocode/lafs';
2
2
  export type { AgentEntry, BundleDiagnostic, CompiledBundle, ParsedCantDocument, TeamEntry, ToolEntry, } from './bundle';
3
3
  export { compileBundle } from './bundle';
4
- export type { AgentDefinition, ContextProvider, ContextSlice, MentalModelSlice, SpawnPayload, Tier, } from './composer.js';
4
+ export type { AgentDefinition, ContextProvider, ContextSlice, MentalModelSlice, PathPermissions, SpawnPayload, Tier, } from './composer.js';
5
5
  export { composeSpawnPayload, escalateTier, estimateTokens, TIER_CAPS } from './composer.js';
6
+ export { brainContextProvider } from './context-provider-brain.js';
6
7
  export type { CantDocumentResult, CantListResult, CantPipelineResult, CantValidationResult, SectionKind, } from './document';
7
8
  export { executePipeline, listSections, parseDocument, validateDocument, } from './document';
8
9
  export type { Role, SpawnValidation, TeamDefinition, TeamRouting } from './hierarchy.js';
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  // JIT Agent Composer (ULTRAPLAN Wave 5)
3
+ // Wave 7a: BRAIN-backed ContextProvider (T432)
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.resolveWorktreeRoot = exports.mergeWorktree = exports.listWorktrees = exports.createWorktree = exports.parseCANTMessage = exports.initCantParser = exports.isWasmAvailable = exports.isNativeAvailable = exports.initWasm = 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.validateSpawnRequest = exports.ORCHESTRATOR_FORBIDDEN_TOOLS = exports.LEAD_FORBIDDEN_TOOLS = exports.filterToolsForRole = exports.validateDocument = exports.parseDocument = exports.listSections = exports.executePipeline = exports.TIER_CAPS = exports.estimateTokens = exports.escalateTier = exports.composeSpawnPayload = exports.compileBundle = void 0;
5
+ exports.resolveWorktreeRoot = exports.mergeWorktree = exports.listWorktrees = exports.createWorktree = exports.parseCANTMessage = exports.initCantParser = exports.isWasmAvailable = exports.isNativeAvailable = exports.initWasm = 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.validateSpawnRequest = 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.compileBundle = void 0;
5
6
  // Bundle compiler
6
7
  var bundle_1 = require("./bundle");
7
8
  Object.defineProperty(exports, "compileBundle", { enumerable: true, get: function () { return bundle_1.compileBundle; } });
@@ -10,6 +11,8 @@ Object.defineProperty(exports, "composeSpawnPayload", { enumerable: true, get: f
10
11
  Object.defineProperty(exports, "escalateTier", { enumerable: true, get: function () { return composer_js_1.escalateTier; } });
11
12
  Object.defineProperty(exports, "estimateTokens", { enumerable: true, get: function () { return composer_js_1.estimateTokens; } });
12
13
  Object.defineProperty(exports, "TIER_CAPS", { enumerable: true, get: function () { return composer_js_1.TIER_CAPS; } });
14
+ var context_provider_brain_js_1 = require("./context-provider-brain.js");
15
+ Object.defineProperty(exports, "brainContextProvider", { enumerable: true, get: function () { return context_provider_brain_js_1.brainContextProvider; } });
13
16
  // High-level API (replaces standalone cant-cli binary)
14
17
  var document_1 = require("./document");
15
18
  Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return document_1.executePipeline; } });
@@ -18,7 +18,16 @@ export interface WorktreeRequest {
18
18
  /** Why this worktree is being created. */
19
19
  reason: 'subagent' | 'experiment' | 'parallel-wave';
20
20
  }
21
- /** Handle returned after worktree creation; used for merge and cleanup. */
21
+ /**
22
+ * Handle returned after worktree creation; used for merge, env-var binding, and cleanup.
23
+ *
24
+ * @remarks
25
+ * The `projectHash` field was added in T380/ADR-041 so callers can populate
26
+ * `CLEO_PROJECT_HASH` in spawned subagent environments without threading
27
+ * {@link WorktreeConfig} through every call site.
28
+ *
29
+ * @task T380
30
+ */
22
31
  export interface WorktreeHandle {
23
32
  /** Absolute path to the worktree directory. */
24
33
  path: string;
@@ -28,6 +37,17 @@ export interface WorktreeHandle {
28
37
  baseRef: string;
29
38
  /** Task ID. */
30
39
  taskId: string;
40
+ /**
41
+ * Project hash used to scope this worktree under the XDG worktree root.
42
+ *
43
+ * @remarks
44
+ * Sourced from {@link WorktreeConfig.projectHash} at creation time.
45
+ * Exposed here so spawn adapters can populate the `CLEO_PROJECT_HASH`
46
+ * environment variable without re-threading the full config.
47
+ *
48
+ * @task T380
49
+ */
50
+ projectHash: string;
31
51
  /** Clean up: remove the worktree and optionally delete the branch. */
32
52
  cleanup(deleteBranch?: boolean): void;
33
53
  }
package/dist/worktree.js CHANGED
@@ -71,7 +71,7 @@ function createWorktree(request, config) {
71
71
  cwd: config.gitRoot,
72
72
  stdio: 'pipe',
73
73
  });
74
- return buildHandle(worktreePath, branch, request.baseRef, request.taskId, config.gitRoot);
74
+ return buildHandle(worktreePath, branch, request.baseRef, request.taskId, config.gitRoot, config.projectHash);
75
75
  }
76
76
  /**
77
77
  * Merge a worktree's branch back into the current branch.
@@ -148,14 +148,20 @@ function listWorktrees(config) {
148
148
  /**
149
149
  * Build a WorktreeHandle with a cleanup closure.
150
150
  *
151
+ * @remarks
152
+ * The `projectHash` parameter was added in T380/ADR-041 and is stored on the
153
+ * returned handle so spawn adapters can populate `CLEO_PROJECT_HASH` without
154
+ * re-threading {@link WorktreeConfig}.
155
+ *
151
156
  * @internal
152
157
  */
153
- function buildHandle(worktreePath, branch, baseRef, taskId, gitRoot) {
158
+ function buildHandle(worktreePath, branch, baseRef, taskId, gitRoot, projectHash) {
154
159
  return {
155
160
  path: worktreePath,
156
161
  branch,
157
162
  baseRef,
158
163
  taskId,
164
+ projectHash,
159
165
  cleanup(deleteBranch = false) {
160
166
  try {
161
167
  (0, node_child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/cant",
3
- "version": "2026.4.15",
3
+ "version": "2026.4.17",
4
4
  "description": "CANT protocol parser and runtime for CLEO — wraps cant-core via napi-rs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,8 +9,9 @@
9
9
  "napi/"
10
10
  ],
11
11
  "dependencies": {
12
- "@cleocode/lafs": "2026.4.15",
13
- "@cleocode/contracts": "2026.4.15"
12
+ "@cleocode/contracts": "2026.4.17",
13
+ "@cleocode/core": "2026.4.17",
14
+ "@cleocode/lafs": "2026.4.17"
14
15
  },
15
16
  "devDependencies": {
16
17
  "typescript": "^6.0.2",