@cleocode/cant 2026.4.11 → 2026.4.13

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Mental Model Manager -- per-agent persistent observations.
3
+ *
4
+ * Implements ULTRAPLAN section 12: mental models are per-project, async-updated
5
+ * after each spawn, and validated on every load.
6
+ *
7
+ * Mental models compound intelligence over time by recording patterns,
8
+ * decisions, and outcomes from agent sessions. They decay naturally and
9
+ * are token-bounded to fit within tier caps.
10
+ *
11
+ * @packageDocumentation
12
+ */
13
+ /** Trigger events that cause an observation to be recorded. */
14
+ export type ObservationTrigger = 'task_completed' | 'bug_fixed' | 'pattern_observed' | 'decision_made';
15
+ /** A single observation in the mental model. */
16
+ export interface MentalModelObservation {
17
+ /** Unique observation ID. */
18
+ id: string;
19
+ /** The agent that made this observation. */
20
+ agentName: string;
21
+ /** Project hash scoping this observation. */
22
+ projectHash: string;
23
+ /** When this observation was recorded (ISO 8601). */
24
+ timestamp: string;
25
+ /** The observation content. */
26
+ content: string;
27
+ /** Estimated tokens for this content. */
28
+ tokens: number;
29
+ /** What triggered this observation. */
30
+ trigger: ObservationTrigger;
31
+ /** Number of times this observation has been reinforced. */
32
+ reinforceCount: number;
33
+ }
34
+ /** Scope for a mental model: project-specific or global. */
35
+ export type MentalModelScope = 'project' | 'global';
36
+ /** The consolidated mental model for an agent+project. */
37
+ export interface MentalModel {
38
+ /** Agent this model belongs to. */
39
+ agentName: string;
40
+ /** Project hash scoping this model. */
41
+ projectHash: string;
42
+ /** Scope: project or global (per ULTRAPLAN L5). */
43
+ scope: MentalModelScope;
44
+ /** Consolidated observations (token-bounded). */
45
+ observations: MentalModelObservation[];
46
+ /** Total tokens across all observations. */
47
+ totalTokens: number;
48
+ /** Max allowed tokens (from tier cap). */
49
+ maxTokens: number;
50
+ /** When this model was last consolidated (ISO 8601), or null if never. */
51
+ lastConsolidated: string | null;
52
+ /** When this model was last validated by an agent (ISO 8601), or null if never. */
53
+ lastValidated: string | null;
54
+ }
55
+ /** Storage interface for mental model persistence (mockable). */
56
+ export interface MentalModelStore {
57
+ /** Load the mental model for an agent+project. */
58
+ load(agentName: string, projectHash: string): Promise<MentalModel | null>;
59
+ /** Save the mental model. */
60
+ save(model: MentalModel): Promise<void>;
61
+ /** Append a raw observation (async, non-blocking per ULTRAPLAN L5). */
62
+ appendObservation(obs: MentalModelObservation): Promise<void>;
63
+ /** List pending (unconsolidated) observations. */
64
+ listPending(agentName: string, projectHash: string): Promise<MentalModelObservation[]>;
65
+ /** Clear pending observations after consolidation. */
66
+ clearPending(agentName: string, projectHash: string): Promise<void>;
67
+ }
68
+ /** Options for the {@link consolidate} function. */
69
+ export interface ConsolidateOptions {
70
+ /** Maximum total tokens for all observations. */
71
+ maxTokens: number;
72
+ /** Number of days after which unreinforced observations decay. */
73
+ decayAfterDays: number;
74
+ /** Scope for the resulting model. */
75
+ scope: MentalModelScope;
76
+ }
77
+ /**
78
+ * Create an empty mental model for an agent+project.
79
+ *
80
+ * @param agentName - The agent this model belongs to.
81
+ * @param projectHash - The project hash scoping this model.
82
+ * @param scope - Whether this is a project or global model.
83
+ * @param maxTokens - Maximum token budget for observations.
84
+ * @returns A fresh, empty mental model.
85
+ */
86
+ export declare function createEmptyModel(agentName: string, projectHash: string, scope: MentalModelScope, maxTokens: number): MentalModel;
87
+ /**
88
+ * Consolidate pending observations into the mental model.
89
+ *
90
+ * Implements the consolidation from ULTRAPLAN section 12.2:
91
+ * 1. Load pending observations from the store
92
+ * 2. Merge into existing model (deduplicate by content, reinforce matches)
93
+ * 3. Decay entries older than decayAfterDays
94
+ * 4. Enforce maxTokens cap (drop oldest first)
95
+ * 5. Save consolidated model and clear pending queue
96
+ *
97
+ * @param store - The persistence layer for mental models.
98
+ * @param agentName - The agent whose model to consolidate.
99
+ * @param projectHash - The project hash scoping the model.
100
+ * @param options - Consolidation configuration.
101
+ * @returns The consolidated mental model.
102
+ */
103
+ export declare function consolidate(store: MentalModelStore, agentName: string, projectHash: string, options: ConsolidateOptions): Promise<MentalModel>;
104
+ /**
105
+ * Render a mental model for system prompt injection.
106
+ *
107
+ * Includes the ULTRAPLAN section 12.3 validation prefix:
108
+ * "VALIDATE THIS MENTAL MODEL. Re-evaluate each claim against
109
+ * current code state. Mental models are dynamic per project;
110
+ * assume drift."
111
+ *
112
+ * @param model - The mental model to render.
113
+ * @returns Rendered text for injection into system prompts, or empty string if no observations.
114
+ */
115
+ export declare function renderMentalModel(model: MentalModel): string;
116
+ /**
117
+ * Session output data used by {@link harvestObservations} to extract
118
+ * mental model observations from a completed agent session.
119
+ */
120
+ export interface SessionOutput {
121
+ /** Patterns the agent applied during the session. */
122
+ patternsUsed: string[];
123
+ /** Decisions the agent made during the session. */
124
+ decisionsMade: string[];
125
+ /** File paths the agent touched during the session. */
126
+ filesTouched: string[];
127
+ /** Overall outcome of the session. */
128
+ outcome: 'success' | 'failure' | 'partial';
129
+ }
130
+ /**
131
+ * Extract observations from a completed agent session.
132
+ *
133
+ * Implements ULTRAPLAN section 12.2 post-spawn harvesting:
134
+ * parses session output for patterns, decisions, and files touched.
135
+ * Only records task completion observations on successful outcomes.
136
+ *
137
+ * @param agentName - The agent that ran the session.
138
+ * @param projectHash - The project hash for scoping observations.
139
+ * @param sessionOutput - Structured output from the completed session.
140
+ * @returns Array of observations ready for {@link MentalModelStore.appendObservation}.
141
+ */
142
+ export declare function harvestObservations(agentName: string, projectHash: string, sessionOutput: SessionOutput): MentalModelObservation[];
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ /**
3
+ * Mental Model Manager -- per-agent persistent observations.
4
+ *
5
+ * Implements ULTRAPLAN section 12: mental models are per-project, async-updated
6
+ * after each spawn, and validated on every load.
7
+ *
8
+ * Mental models compound intelligence over time by recording patterns,
9
+ * decisions, and outcomes from agent sessions. They decay naturally and
10
+ * are token-bounded to fit within tier caps.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.createEmptyModel = createEmptyModel;
16
+ exports.consolidate = consolidate;
17
+ exports.renderMentalModel = renderMentalModel;
18
+ exports.harvestObservations = harvestObservations;
19
+ // ---------------------------------------------------------------------------
20
+ // Validation prefix (ULTRAPLAN section 12.3)
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Validation prefix injected before mental model content.
24
+ *
25
+ * Per ULTRAPLAN section 12.3, every load must instruct the agent to
26
+ * re-evaluate the mental model against current project state.
27
+ */
28
+ const VALIDATION_PREFIX = 'VALIDATE THIS MENTAL MODEL. Re-evaluate each claim against current ' +
29
+ 'code state. Mental models are dynamic per project; assume drift.';
30
+ // ---------------------------------------------------------------------------
31
+ // Core functions
32
+ // ---------------------------------------------------------------------------
33
+ /**
34
+ * Create an empty mental model for an agent+project.
35
+ *
36
+ * @param agentName - The agent this model belongs to.
37
+ * @param projectHash - The project hash scoping this model.
38
+ * @param scope - Whether this is a project or global model.
39
+ * @param maxTokens - Maximum token budget for observations.
40
+ * @returns A fresh, empty mental model.
41
+ */
42
+ function createEmptyModel(agentName, projectHash, scope, maxTokens) {
43
+ return {
44
+ agentName,
45
+ projectHash,
46
+ scope,
47
+ observations: [],
48
+ totalTokens: 0,
49
+ maxTokens,
50
+ lastConsolidated: null,
51
+ lastValidated: null,
52
+ };
53
+ }
54
+ /**
55
+ * Consolidate pending observations into the mental model.
56
+ *
57
+ * Implements the consolidation from ULTRAPLAN section 12.2:
58
+ * 1. Load pending observations from the store
59
+ * 2. Merge into existing model (deduplicate by content, reinforce matches)
60
+ * 3. Decay entries older than decayAfterDays
61
+ * 4. Enforce maxTokens cap (drop oldest first)
62
+ * 5. Save consolidated model and clear pending queue
63
+ *
64
+ * @param store - The persistence layer for mental models.
65
+ * @param agentName - The agent whose model to consolidate.
66
+ * @param projectHash - The project hash scoping the model.
67
+ * @param options - Consolidation configuration.
68
+ * @returns The consolidated mental model.
69
+ */
70
+ async function consolidate(store, agentName, projectHash, options) {
71
+ // Load existing model or create fresh
72
+ let model = await store.load(agentName, projectHash);
73
+ if (!model) {
74
+ model = createEmptyModel(agentName, projectHash, options.scope, options.maxTokens);
75
+ }
76
+ // Load pending observations
77
+ const pending = await store.listPending(agentName, projectHash);
78
+ // Merge: add pending, increment reinforceCount for duplicates
79
+ for (const obs of pending) {
80
+ const existing = model.observations.find((o) => o.content === obs.content);
81
+ if (existing) {
82
+ existing.reinforceCount += 1;
83
+ existing.timestamp = obs.timestamp; // refresh timestamp on reinforce
84
+ }
85
+ else {
86
+ model.observations.push(obs);
87
+ }
88
+ }
89
+ // Decay: remove observations older than decayAfterDays
90
+ const cutoff = new Date();
91
+ cutoff.setDate(cutoff.getDate() - options.decayAfterDays);
92
+ const cutoffIso = cutoff.toISOString();
93
+ model.observations = model.observations.filter((o) => o.timestamp >= cutoffIso);
94
+ // Enforce token cap: sort newest first, keep within budget
95
+ model.observations.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
96
+ let tokenSum = 0;
97
+ const kept = [];
98
+ for (const obs of model.observations) {
99
+ if (tokenSum + obs.tokens > options.maxTokens)
100
+ break;
101
+ kept.push(obs);
102
+ tokenSum += obs.tokens;
103
+ }
104
+ model.observations = kept;
105
+ model.totalTokens = tokenSum;
106
+ model.maxTokens = options.maxTokens;
107
+ model.lastConsolidated = new Date().toISOString();
108
+ // Persist consolidated model and clear pending queue
109
+ await store.save(model);
110
+ await store.clearPending(agentName, projectHash);
111
+ return model;
112
+ }
113
+ /**
114
+ * Render a mental model for system prompt injection.
115
+ *
116
+ * Includes the ULTRAPLAN section 12.3 validation prefix:
117
+ * "VALIDATE THIS MENTAL MODEL. Re-evaluate each claim against
118
+ * current code state. Mental models are dynamic per project;
119
+ * assume drift."
120
+ *
121
+ * @param model - The mental model to render.
122
+ * @returns Rendered text for injection into system prompts, or empty string if no observations.
123
+ */
124
+ function renderMentalModel(model) {
125
+ if (model.observations.length === 0)
126
+ return '';
127
+ const entries = model.observations
128
+ .map((o) => `- [${o.trigger}] ${o.content} (reinforced ${o.reinforceCount}x)`)
129
+ .join('\n');
130
+ return `${VALIDATION_PREFIX}\n\n${entries}`;
131
+ }
132
+ /**
133
+ * Generate a unique observation ID.
134
+ *
135
+ * @returns A prefixed unique ID string.
136
+ */
137
+ function generateObservationId() {
138
+ return `mm-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
139
+ }
140
+ /**
141
+ * Extract observations from a completed agent session.
142
+ *
143
+ * Implements ULTRAPLAN section 12.2 post-spawn harvesting:
144
+ * parses session output for patterns, decisions, and files touched.
145
+ * Only records task completion observations on successful outcomes.
146
+ *
147
+ * @param agentName - The agent that ran the session.
148
+ * @param projectHash - The project hash for scoping observations.
149
+ * @param sessionOutput - Structured output from the completed session.
150
+ * @returns Array of observations ready for {@link MentalModelStore.appendObservation}.
151
+ */
152
+ function harvestObservations(agentName, projectHash, sessionOutput) {
153
+ const now = new Date().toISOString();
154
+ const observations = [];
155
+ for (const pattern of sessionOutput.patternsUsed) {
156
+ observations.push({
157
+ id: generateObservationId(),
158
+ agentName,
159
+ projectHash,
160
+ timestamp: now,
161
+ content: `Pattern applied: ${pattern}`,
162
+ tokens: Math.ceil(`Pattern applied: ${pattern}`.length / 4),
163
+ trigger: 'pattern_observed',
164
+ reinforceCount: 0,
165
+ });
166
+ }
167
+ for (const decision of sessionOutput.decisionsMade) {
168
+ observations.push({
169
+ id: generateObservationId(),
170
+ agentName,
171
+ projectHash,
172
+ timestamp: now,
173
+ content: `Decision: ${decision}`,
174
+ tokens: Math.ceil(`Decision: ${decision}`.length / 4),
175
+ trigger: 'decision_made',
176
+ reinforceCount: 0,
177
+ });
178
+ }
179
+ if (sessionOutput.outcome === 'success') {
180
+ const fileList = sessionOutput.filesTouched.join(', ');
181
+ const content = `Task completed successfully. Files: ${fileList}`;
182
+ observations.push({
183
+ id: generateObservationId(),
184
+ agentName,
185
+ projectHash,
186
+ timestamp: now,
187
+ content,
188
+ tokens: Math.ceil(content.length / 4),
189
+ trigger: 'task_completed',
190
+ reinforceCount: 0,
191
+ });
192
+ }
193
+ return observations;
194
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Git worktree manager for multi-agent isolation.
3
+ *
4
+ * Implements ULTRAPLAN §14: each spawned agent gets its own git
5
+ * worktree so parallel workers cannot conflict. The orchestrator
6
+ * stays on its current branch.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+ /** Request payload for creating a new git worktree. */
11
+ export interface WorktreeRequest {
12
+ /** The base ref to branch from (e.g. "main", "develop", a SHA). */
13
+ baseRef: string;
14
+ /** Branch name for the worktree. If absent, derived from taskId. */
15
+ branchName?: string;
16
+ /** Task ID driving this worktree. */
17
+ taskId: string;
18
+ /** Why this worktree is being created. */
19
+ reason: 'subagent' | 'experiment' | 'parallel-wave';
20
+ }
21
+ /** Handle returned after worktree creation; used for merge and cleanup. */
22
+ export interface WorktreeHandle {
23
+ /** Absolute path to the worktree directory. */
24
+ path: string;
25
+ /** Branch name created for this worktree. */
26
+ branch: string;
27
+ /** The base ref it was branched from. */
28
+ baseRef: string;
29
+ /** Task ID. */
30
+ taskId: string;
31
+ /** Clean up: remove the worktree and optionally delete the branch. */
32
+ cleanup(deleteBranch?: boolean): void;
33
+ }
34
+ /** Configuration for worktree path resolution and git operations. */
35
+ export interface WorktreeConfig {
36
+ /** Root directory for worktrees. Defaults to $XDG_DATA_HOME/cleo/worktrees/<projectHash>/ */
37
+ worktreeRoot?: string;
38
+ /** Project hash for path scoping. */
39
+ projectHash: string;
40
+ /** The project's git root directory. */
41
+ gitRoot: string;
42
+ }
43
+ /** Result of a merge operation. */
44
+ export interface MergeResult {
45
+ /** Whether the merge succeeded. */
46
+ success: boolean;
47
+ /** Error message if the merge failed. */
48
+ error?: string;
49
+ }
50
+ /** Entry in the list of active worktrees. */
51
+ export interface WorktreeEntry {
52
+ /** Absolute path to the worktree directory. */
53
+ path: string;
54
+ /** Branch checked out in this worktree. */
55
+ branch: string;
56
+ }
57
+ /**
58
+ * Resolve the worktree root directory.
59
+ *
60
+ * Uses `$XDG_DATA_HOME/cleo/worktrees/<projectHash>/` per ULTRAPLAN §14.3.
61
+ * Falls back to `~/.local/share/cleo/worktrees/<projectHash>/` when
62
+ * `XDG_DATA_HOME` is not set.
63
+ *
64
+ * @param config - Worktree configuration containing optional override and project hash.
65
+ * @returns Absolute path to the worktree root directory.
66
+ */
67
+ export declare function resolveWorktreeRoot(config: WorktreeConfig): string;
68
+ /**
69
+ * Create a git worktree for an agent.
70
+ *
71
+ * Runs `git worktree add <path> -b <branch> <baseRef>` from the
72
+ * project's git root. Branch naming convention: `cleo/<taskId>-<shortId>`.
73
+ *
74
+ * If a worktree already exists at the target path (stale from a prior run),
75
+ * it is removed before creating the new one.
76
+ *
77
+ * @param request - The worktree request specifying task, base ref, and optional branch name.
78
+ * @param config - Project-level worktree configuration.
79
+ * @returns A handle for the created worktree with cleanup capability.
80
+ * @throws Error if the git worktree add command fails.
81
+ */
82
+ export declare function createWorktree(request: WorktreeRequest, config: WorktreeConfig): WorktreeHandle;
83
+ /**
84
+ * Merge a worktree's branch back into the current branch.
85
+ *
86
+ * On success the worktree is cleaned up (directory removed, branch deleted).
87
+ * On failure the worktree is retained for forensic inspection.
88
+ *
89
+ * @param handle - The worktree handle returned from {@link createWorktree}.
90
+ * @param config - Project-level worktree configuration.
91
+ * @param options - Merge strategy options (defaults to fast-forward only).
92
+ * @returns A result object indicating success or failure with an error message.
93
+ */
94
+ export declare function mergeWorktree(handle: WorktreeHandle, config: WorktreeConfig, options?: {
95
+ strategy?: 'ff-only' | 'no-ff';
96
+ }): MergeResult;
97
+ /**
98
+ * List active worktrees scoped to the current project.
99
+ *
100
+ * Parses `git worktree list --porcelain` and filters entries whose path
101
+ * falls under the project's worktree root directory.
102
+ *
103
+ * @param config - Project-level worktree configuration.
104
+ * @returns Array of worktree entries with their paths and branch names.
105
+ */
106
+ export declare function listWorktrees(config: WorktreeConfig): WorktreeEntry[];
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ /**
3
+ * Git worktree manager for multi-agent isolation.
4
+ *
5
+ * Implements ULTRAPLAN §14: each spawned agent gets its own git
6
+ * worktree so parallel workers cannot conflict. The orchestrator
7
+ * stays on its current branch.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.resolveWorktreeRoot = resolveWorktreeRoot;
13
+ exports.createWorktree = createWorktree;
14
+ exports.mergeWorktree = mergeWorktree;
15
+ exports.listWorktrees = listWorktrees;
16
+ const node_child_process_1 = require("node:child_process");
17
+ const node_fs_1 = require("node:fs");
18
+ const node_path_1 = require("node:path");
19
+ const node_os_1 = require("node:os");
20
+ /**
21
+ * Resolve the worktree root directory.
22
+ *
23
+ * Uses `$XDG_DATA_HOME/cleo/worktrees/<projectHash>/` per ULTRAPLAN §14.3.
24
+ * Falls back to `~/.local/share/cleo/worktrees/<projectHash>/` when
25
+ * `XDG_DATA_HOME` is not set.
26
+ *
27
+ * @param config - Worktree configuration containing optional override and project hash.
28
+ * @returns Absolute path to the worktree root directory.
29
+ */
30
+ function resolveWorktreeRoot(config) {
31
+ if (config.worktreeRoot)
32
+ return config.worktreeRoot;
33
+ const xdgData = process.env['XDG_DATA_HOME'] ?? (0, node_path_1.join)((0, node_os_1.homedir)(), '.local', 'share');
34
+ return (0, node_path_1.join)(xdgData, 'cleo', 'worktrees', config.projectHash);
35
+ }
36
+ /**
37
+ * Create a git worktree for an agent.
38
+ *
39
+ * Runs `git worktree add <path> -b <branch> <baseRef>` from the
40
+ * project's git root. Branch naming convention: `cleo/<taskId>-<shortId>`.
41
+ *
42
+ * If a worktree already exists at the target path (stale from a prior run),
43
+ * it is removed before creating the new one.
44
+ *
45
+ * @param request - The worktree request specifying task, base ref, and optional branch name.
46
+ * @param config - Project-level worktree configuration.
47
+ * @returns A handle for the created worktree with cleanup capability.
48
+ * @throws Error if the git worktree add command fails.
49
+ */
50
+ function createWorktree(request, config) {
51
+ const root = resolveWorktreeRoot(config);
52
+ (0, node_fs_1.mkdirSync)(root, { recursive: true });
53
+ const shortId = Math.random().toString(36).slice(2, 8);
54
+ const branch = request.branchName ?? `cleo/${request.taskId}-${shortId}`;
55
+ const worktreePath = (0, node_path_1.join)(root, request.taskId);
56
+ // Remove existing worktree at this path if it exists (stale from prior run)
57
+ if ((0, node_fs_1.existsSync)(worktreePath)) {
58
+ try {
59
+ (0, node_child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
60
+ cwd: config.gitRoot,
61
+ stdio: 'pipe',
62
+ });
63
+ }
64
+ catch {
65
+ // Best-effort cleanup — directory may not be a valid worktree
66
+ (0, node_fs_1.rmSync)(worktreePath, { recursive: true, force: true });
67
+ }
68
+ }
69
+ // Create the worktree
70
+ (0, node_child_process_1.execSync)(`git worktree add "${worktreePath}" -b "${branch}" "${request.baseRef}"`, { cwd: config.gitRoot, stdio: 'pipe' });
71
+ return buildHandle(worktreePath, branch, request.baseRef, request.taskId, config.gitRoot);
72
+ }
73
+ /**
74
+ * Merge a worktree's branch back into the current branch.
75
+ *
76
+ * On success the worktree is cleaned up (directory removed, branch deleted).
77
+ * On failure the worktree is retained for forensic inspection.
78
+ *
79
+ * @param handle - The worktree handle returned from {@link createWorktree}.
80
+ * @param config - Project-level worktree configuration.
81
+ * @param options - Merge strategy options (defaults to fast-forward only).
82
+ * @returns A result object indicating success or failure with an error message.
83
+ */
84
+ function mergeWorktree(handle, config, options = {}) {
85
+ const strategy = options.strategy ?? 'ff-only';
86
+ const mergeFlag = strategy === 'ff-only' ? '--ff-only' : '--no-ff';
87
+ try {
88
+ (0, node_child_process_1.execSync)(`git merge ${mergeFlag} "${handle.branch}"`, {
89
+ cwd: config.gitRoot,
90
+ stdio: 'pipe',
91
+ });
92
+ // Success: clean up worktree
93
+ handle.cleanup(true);
94
+ return { success: true };
95
+ }
96
+ catch (err) {
97
+ // Failure: retain worktree for forensics
98
+ const message = err instanceof Error ? err.message : String(err);
99
+ return {
100
+ success: false,
101
+ error: `Merge failed: ${message}. Worktree retained at ${handle.path} for forensics.`,
102
+ };
103
+ }
104
+ }
105
+ /**
106
+ * List active worktrees scoped to the current project.
107
+ *
108
+ * Parses `git worktree list --porcelain` and filters entries whose path
109
+ * falls under the project's worktree root directory.
110
+ *
111
+ * @param config - Project-level worktree configuration.
112
+ * @returns Array of worktree entries with their paths and branch names.
113
+ */
114
+ function listWorktrees(config) {
115
+ try {
116
+ const output = (0, node_child_process_1.execSync)('git worktree list --porcelain', {
117
+ cwd: config.gitRoot,
118
+ encoding: 'utf-8',
119
+ });
120
+ const entries = [];
121
+ const root = resolveWorktreeRoot(config);
122
+ let currentPath = '';
123
+ let currentBranch = '';
124
+ for (const line of output.split('\n')) {
125
+ if (line.startsWith('worktree ')) {
126
+ currentPath = line.slice('worktree '.length);
127
+ }
128
+ else if (line.startsWith('branch ')) {
129
+ currentBranch = line.slice('branch refs/heads/'.length);
130
+ }
131
+ else if (line === '') {
132
+ if (currentPath.startsWith(root)) {
133
+ entries.push({ path: currentPath, branch: currentBranch });
134
+ }
135
+ currentPath = '';
136
+ currentBranch = '';
137
+ }
138
+ }
139
+ return entries;
140
+ }
141
+ catch {
142
+ return [];
143
+ }
144
+ }
145
+ /**
146
+ * Build a WorktreeHandle with a cleanup closure.
147
+ *
148
+ * @internal
149
+ */
150
+ function buildHandle(worktreePath, branch, baseRef, taskId, gitRoot) {
151
+ return {
152
+ path: worktreePath,
153
+ branch,
154
+ baseRef,
155
+ taskId,
156
+ cleanup(deleteBranch = false) {
157
+ try {
158
+ (0, node_child_process_1.execSync)(`git worktree remove "${worktreePath}" --force`, {
159
+ cwd: gitRoot,
160
+ stdio: 'pipe',
161
+ });
162
+ }
163
+ catch {
164
+ (0, node_fs_1.rmSync)(worktreePath, { recursive: true, force: true });
165
+ }
166
+ if (deleteBranch) {
167
+ try {
168
+ (0, node_child_process_1.execSync)(`git branch -D "${branch}"`, {
169
+ cwd: gitRoot,
170
+ stdio: 'pipe',
171
+ });
172
+ }
173
+ catch {
174
+ // Branch may already be deleted
175
+ }
176
+ }
177
+ },
178
+ };
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/cant",
3
- "version": "2026.4.11",
3
+ "version": "2026.4.13",
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,8 @@
9
9
  "napi/"
10
10
  ],
11
11
  "dependencies": {
12
- "@cleocode/contracts": "2026.4.11",
13
- "@cleocode/lafs": "2026.4.11"
12
+ "@cleocode/contracts": "2026.4.13",
13
+ "@cleocode/lafs": "2026.4.13"
14
14
  },
15
15
  "devDependencies": {
16
16
  "typescript": "^5.0.0",