@cleocode/contracts 2026.4.114 → 2026.4.116

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,295 @@
1
+ /**
2
+ * Worktree backend SDK operation types (T1161).
3
+ *
4
+ * Wire-format types for the `@cleocode/worktree-backend` SDK surface.
5
+ * Defines the contract for createWorktree, destroyWorktree, listWorktrees,
6
+ * and pruneWorktrees operations with XDG path canon (D029) and declarative
7
+ * hooks (D030 native lift of worktrunk missing features).
8
+ *
9
+ * @task T1161
10
+ * @adr ADR-055
11
+ */
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Hooks framework
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /**
18
+ * A declarative lifecycle hook definition for worktree events.
19
+ *
20
+ * Hooks are executed synchronously (shell commands) or via node spawns
21
+ * in the worktree directory. Mirrors the worktrunk hooks contract lifted
22
+ * natively per D030.
23
+ *
24
+ * @task T1161
25
+ */
26
+ export interface WorktreeHook {
27
+ /** Shell command to run (executed via `sh -c` in the worktree dir). */
28
+ command: string;
29
+ /**
30
+ * When to run the hook.
31
+ *
32
+ * - `post-create` — immediately after `git worktree add` succeeds.
33
+ * - `post-start` — after the agent CWD is established (env vars injected).
34
+ */
35
+ event: 'post-create' | 'post-start';
36
+ /**
37
+ * Optional timeout in milliseconds before the hook is killed.
38
+ *
39
+ * @default 30000
40
+ */
41
+ timeoutMs?: number;
42
+ /**
43
+ * When true, a non-zero exit from this hook causes the worktree operation
44
+ * to fail. When false (default), errors are logged but ignored.
45
+ *
46
+ * @default false
47
+ */
48
+ failOnError?: boolean;
49
+ }
50
+
51
+ /**
52
+ * Result of a single hook execution.
53
+ *
54
+ * @task T1161
55
+ */
56
+ export interface WorktreeHookResult {
57
+ /** The hook that was executed. */
58
+ hook: WorktreeHook;
59
+ /** Whether the hook exited with code 0. */
60
+ success: boolean;
61
+ /** Hook stdout (trimmed). */
62
+ stdout: string;
63
+ /** Hook stderr (trimmed). */
64
+ stderr: string;
65
+ /** Exit code (null if killed by timeout). */
66
+ exitCode: number | null;
67
+ /** Duration in milliseconds. */
68
+ durationMs: number;
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Worktree-include glob pattern
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /**
76
+ * A parsed entry from `.cleo/worktree-include`.
77
+ *
78
+ * The file lists glob patterns (one per line, `#` comments stripped) that
79
+ * control which files from the main project tree are symlinked or copied
80
+ * into new worktrees on creation. This is a native lift of the worktrunk
81
+ * `.cleo/worktree-include` feature (D030).
82
+ *
83
+ * @task T1161
84
+ */
85
+ export interface WorktreeIncludePattern {
86
+ /** The raw glob pattern string (e.g. `node_modules/.pnpm/**`). */
87
+ pattern: string;
88
+ /**
89
+ * Whether this entry was negated (prefixed with `!`).
90
+ *
91
+ * @default false
92
+ */
93
+ negated: boolean;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Create operation
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /**
101
+ * Options for creating a new agent worktree.
102
+ *
103
+ * @task T1161
104
+ */
105
+ export interface CreateWorktreeOptions {
106
+ /** The task ID that will own this worktree. */
107
+ taskId: string;
108
+ /** The base ref to branch from (default: current HEAD). */
109
+ baseRef?: string;
110
+ /** Explicit branch name. Defaults to `task/<taskId>`. */
111
+ branchName?: string;
112
+ /**
113
+ * Reason for creation. Used for git worktree lock comment and audit logs.
114
+ *
115
+ * @default 'subagent'
116
+ */
117
+ reason?: 'subagent' | 'experiment' | 'parallel-wave';
118
+ /** Declarative hooks to run after creation. */
119
+ hooks?: WorktreeHook[];
120
+ /**
121
+ * When true, read `.cleo/worktree-include` from the project root and apply
122
+ * any declared include patterns to the new worktree.
123
+ *
124
+ * @default true
125
+ */
126
+ applyIncludePatterns?: boolean;
127
+ /**
128
+ * When true, apply git worktree lock (`git worktree lock`) to prevent
129
+ * accidental pruning by git.
130
+ *
131
+ * @default true
132
+ */
133
+ lockWorktree?: boolean;
134
+ }
135
+
136
+ /**
137
+ * Result of a successful worktree creation.
138
+ *
139
+ * @task T1161
140
+ */
141
+ export interface CreateWorktreeResult {
142
+ /**
143
+ * Absolute path to the created worktree directory.
144
+ *
145
+ * Always under `~/.local/share/cleo/worktrees/<projectHash>/<taskId>/`
146
+ * per D029 canonical layout.
147
+ */
148
+ path: string;
149
+ /** Branch name created for this worktree (format: `task/<taskId>`). */
150
+ branch: string;
151
+ /** Base ref the branch was created from. */
152
+ baseRef: string;
153
+ /** Task ID that owns this worktree. */
154
+ taskId: string;
155
+ /** Project hash scoping this worktree under the XDG root. */
156
+ projectHash: string;
157
+ /** ISO 8601 timestamp when the worktree was created. */
158
+ createdAt: string;
159
+ /** Whether git worktree lock was applied. */
160
+ locked: boolean;
161
+ /** Environment variables to inject into the spawned agent process. */
162
+ envVars: Record<string, string>;
163
+ /** Prompt preamble text for agent isolation context. */
164
+ preamble: string;
165
+ /** Results of any post-create hooks that were executed. */
166
+ hookResults: WorktreeHookResult[];
167
+ /** Include patterns that were applied (empty if none). */
168
+ appliedPatterns: WorktreeIncludePattern[];
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // Destroy operation
173
+ // ---------------------------------------------------------------------------
174
+
175
+ /**
176
+ * Options for destroying a worktree.
177
+ *
178
+ * @task T1161
179
+ */
180
+ export interface DestroyWorktreeOptions {
181
+ /** Task ID whose worktree to destroy. */
182
+ taskId: string;
183
+ /**
184
+ * When true, delete the associated git branch after removing the worktree.
185
+ *
186
+ * @default true
187
+ */
188
+ deleteBranch?: boolean;
189
+ /**
190
+ * When true, cherry-pick any commits on the task branch back to the
191
+ * orchestrator's current branch before destroying.
192
+ *
193
+ * @default false
194
+ */
195
+ cherryPickFirst?: boolean;
196
+ }
197
+
198
+ /**
199
+ * Result of a worktree destroy operation.
200
+ *
201
+ * @task T1161
202
+ */
203
+ export interface DestroyWorktreeResult {
204
+ /** Task ID that was destroyed. */
205
+ taskId: string;
206
+ /** Whether the worktree directory was successfully removed. */
207
+ worktreeRemoved: boolean;
208
+ /** Whether the task branch was deleted. */
209
+ branchDeleted: boolean;
210
+ /** Whether cherry-pick was attempted and succeeded. */
211
+ cherryPicked: boolean;
212
+ /** Number of commits cherry-picked (0 if none or not requested). */
213
+ commitCount: number;
214
+ /** Error message if any step failed (non-fatal — caller decides). */
215
+ error?: string;
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // List operation
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * A single entry from the worktree listing.
224
+ *
225
+ * @task T1161
226
+ */
227
+ export interface WorktreeListEntry {
228
+ /** Absolute path to the worktree directory. */
229
+ path: string;
230
+ /** Branch checked out in this worktree. */
231
+ branch: string;
232
+ /**
233
+ * Task ID derived from the worktree directory name.
234
+ *
235
+ * Convention: the last path segment is the taskId.
236
+ */
237
+ taskId: string;
238
+ /** Project hash derived from the path. */
239
+ projectHash: string;
240
+ }
241
+
242
+ /**
243
+ * Options for listing worktrees.
244
+ *
245
+ * @task T1161
246
+ */
247
+ export interface ListWorktreesOptions {
248
+ /** Filter to only return worktrees for a specific project hash. */
249
+ projectHash?: string;
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // Prune operation
254
+ // ---------------------------------------------------------------------------
255
+
256
+ /**
257
+ * Options for pruning orphaned worktrees.
258
+ *
259
+ * @task T1161
260
+ */
261
+ export interface PruneWorktreesOptions {
262
+ /** Absolute path to the project root (used to resolve the worktree root). */
263
+ projectRoot: string;
264
+ /**
265
+ * Set of task IDs to preserve. Any worktree directory whose name is NOT in
266
+ * this set will be pruned.
267
+ *
268
+ * When omitted, only stale git administrative entries are pruned (via
269
+ * `git worktree prune`). No worktree directories are removed.
270
+ */
271
+ preserveTaskIds?: Set<string>;
272
+ /**
273
+ * When true, also run `git worktree prune` to clean up stale git
274
+ * administrative entries even if `preserveTaskIds` is not provided.
275
+ *
276
+ * @default true
277
+ */
278
+ gitPrune?: boolean;
279
+ }
280
+
281
+ /**
282
+ * Result of a prune operation.
283
+ *
284
+ * @task T1161
285
+ */
286
+ export interface PruneWorktreesResult {
287
+ /** Number of worktree directories removed. */
288
+ removed: number;
289
+ /** Absolute paths that were removed. */
290
+ removedPaths: string[];
291
+ /** Entries that failed to remove (with reasons). */
292
+ errors: Array<{ path: string; reason: string }>;
293
+ /** Whether `git worktree prune` was run. */
294
+ gitPruneRan: boolean;
295
+ }
package/src/peer.ts ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * PeerIdentity — canonical type for CANT agent persona records.
3
+ *
4
+ * Introduced by T1210 (v2026.4.110 Wave 0.2) as the SDK-first contract
5
+ * for agent identity. `packages/cant/src/native-loader.ts` produces
6
+ * `PeerIdentity[]` from the canonical seed-agents path; dispatch and
7
+ * CLI layers consume it to drive `cleo agents list` and spawn routing.
8
+ *
9
+ * Design constraints (ADR-055 / D028 boundary rules):
10
+ * - Lives in `packages/contracts/` — ZERO runtime dependencies.
11
+ * - Consumed by `packages/cant/` (loader) and `packages/cleo/` (CLI).
12
+ * - No cross-package relative imports.
13
+ *
14
+ * @module peer
15
+ * @task T1210
16
+ * @epic T1144
17
+ */
18
+
19
+ // ============================================================================
20
+ // PeerKind taxonomy
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Classification of a peer agent's role in the orchestration hierarchy.
25
+ *
26
+ * Mirrors the three-tier model in {@link AgentSpawnCapability}:
27
+ * - `orchestrator` — coordinates multi-agent workflows; may spawn leads and workers
28
+ * - `lead` — specialist; dispatches workers only
29
+ * - `worker` — terminal; executes tasks, cannot spawn
30
+ * - `subagent` — universal base role; resolved to a specific tier at spawn time
31
+ */
32
+ export type PeerKind = 'orchestrator' | 'lead' | 'worker' | 'subagent';
33
+
34
+ // ============================================================================
35
+ // PeerIdentity
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Canonical identity record for a CANT-defined agent persona.
40
+ *
41
+ * Produced by `loadSeedAgentIdentities()` in `packages/cant/src/native-loader.ts`
42
+ * and consumed by the dispatch layer + `cleo agents list`. Every field is
43
+ * required — loaders MUST supply a value for each field, using empty strings
44
+ * for absent optional content in the source `.cant` file.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * import type { PeerIdentity } from '@cleocode/contracts';
49
+ *
50
+ * const personas: PeerIdentity[] = loadSeedAgentIdentities();
51
+ * for (const p of personas) {
52
+ * console.log(`${p.peerId} (${p.peerKind}): ${p.displayName}`);
53
+ * }
54
+ * ```
55
+ *
56
+ * @task T1210
57
+ * @epic T1144
58
+ */
59
+ export interface PeerIdentity {
60
+ /**
61
+ * Stable business identifier for the agent, matching the `agent <id>:` block
62
+ * name in the `.cant` file and the `agents.agent_id` column in the registry.
63
+ *
64
+ * @example `"cleo-prime"`, `"cleo-dev"`, `"cleo-subagent"`
65
+ */
66
+ peerId: string;
67
+
68
+ /**
69
+ * Role classification derived from the `role:` field in the `.cant` agent
70
+ * block. Determines spawn authority in the orchestration hierarchy.
71
+ */
72
+ peerKind: PeerKind;
73
+
74
+ /**
75
+ * Absolute path to the canonical `.cant` file that defines this persona.
76
+ *
77
+ * For seed-agents this is inside `packages/agents/seed-agents/` or
78
+ * `packages/agents/cleo-subagent.cant` (universal base). For project-tier
79
+ * personas it is inside `.cleo/cant/agents/`.
80
+ */
81
+ cantFile: string;
82
+
83
+ /**
84
+ * Human-readable name for the persona, derived from the `display_name:` or
85
+ * `description:` field. Falls back to the `peerId` value when neither field
86
+ * is present in the source `.cant`.
87
+ */
88
+ displayName: string;
89
+
90
+ /**
91
+ * Short description of the persona's purpose, taken from the `description:`
92
+ * field in the `.cant` agent block. Empty string when absent.
93
+ */
94
+ description: string;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Runtime validation
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Validate that an unknown value conforms to the {@link PeerIdentity} shape.
103
+ *
104
+ * Performs a lightweight structural check without Zod to keep `packages/contracts`
105
+ * dependency-free at runtime. Callers that need schema-level validation
106
+ * (e.g., test fixtures) can use {@link assertPeerIdentity}.
107
+ *
108
+ * @param value - Value to test.
109
+ * @returns `true` when `value` is a valid {@link PeerIdentity}.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * if (isPeerIdentity(raw)) {
114
+ * console.log(raw.peerId);
115
+ * }
116
+ * ```
117
+ */
118
+ export function isPeerIdentity(value: unknown): value is PeerIdentity {
119
+ if (typeof value !== 'object' || value === null) return false;
120
+ const v = value as Record<string, unknown>;
121
+ return (
122
+ typeof v['peerId'] === 'string' &&
123
+ v['peerId'].length > 0 &&
124
+ typeof v['peerKind'] === 'string' &&
125
+ ['orchestrator', 'lead', 'worker', 'subagent'].includes(v['peerKind'] as string) &&
126
+ typeof v['cantFile'] === 'string' &&
127
+ v['cantFile'].length > 0 &&
128
+ typeof v['displayName'] === 'string' &&
129
+ typeof v['description'] === 'string'
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Assert that a value is a valid {@link PeerIdentity}, throwing a descriptive
135
+ * error when the shape does not conform.
136
+ *
137
+ * @param value - Value to assert.
138
+ * @throws {TypeError} When the value does not satisfy {@link isPeerIdentity}.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * assertPeerIdentity(raw); // throws if invalid
143
+ * console.log(raw.peerId); // safe
144
+ * ```
145
+ */
146
+ export function assertPeerIdentity(value: unknown): asserts value is PeerIdentity {
147
+ if (!isPeerIdentity(value)) {
148
+ throw new TypeError(
149
+ `Expected PeerIdentity { peerId, peerKind, cantFile, displayName, description } — got: ${JSON.stringify(value)}`,
150
+ );
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Validate and filter an array of unknown values to {@link PeerIdentity}[].
156
+ *
157
+ * Invalid entries are silently dropped. This is the safe variant for loading
158
+ * from untrusted sources (e.g., the result of parsing a directory of `.cant`
159
+ * files whose format may vary).
160
+ *
161
+ * @param values - Array of unknown values.
162
+ * @returns Array containing only values that pass {@link isPeerIdentity}.
163
+ */
164
+ export function filterPeerIdentities(values: unknown[]): PeerIdentity[] {
165
+ return values.filter(isPeerIdentity);
166
+ }
package/src/transport.ts CHANGED
@@ -12,7 +12,12 @@
12
12
  */
13
13
 
14
14
  import type { TransportConfig } from './agent-registry.js';
15
- import type { ConduitMessage } from './conduit.js';
15
+ import type {
16
+ ConduitMessage,
17
+ ConduitTopicPublishOptions,
18
+ ConduitTopicSubscribeOptions,
19
+ ConduitUnsubscribe,
20
+ } from './conduit.js';
16
21
 
17
22
  // ============================================================================
18
23
  // Transport connection config
@@ -61,6 +66,58 @@ export interface Transport {
61
66
 
62
67
  /** Subscribe to real-time events (SSE/WebSocket). Returns unsubscribe. */
63
68
  subscribe?(handler: (message: ConduitMessage) => void): () => void;
69
+
70
+ // ── A2A Topic Operations (T1252 — optional, LocalTransport only) ─────────
71
+
72
+ /**
73
+ * Subscribe agent to a named topic.
74
+ *
75
+ * Optional — only implemented by `LocalTransport`. Cloud transports
76
+ * (HttpTransport, SseTransport) do not yet support topic operations.
77
+ *
78
+ * @param topicName - Topic name, e.g. `"epic-T1149.wave-2"`.
79
+ * @param options - Subscription filter options.
80
+ * @task T1252
81
+ */
82
+ subscribeTopic?(topicName: string, options?: ConduitTopicSubscribeOptions): Promise<void>;
83
+
84
+ /**
85
+ * Publish a message to a named topic.
86
+ *
87
+ * Optional — only implemented by `LocalTransport`.
88
+ *
89
+ * @param topicName - Target topic name.
90
+ * @param content - Message content.
91
+ * @param options - Kind and optional payload.
92
+ * @task T1252
93
+ */
94
+ publishToTopic?(
95
+ topicName: string,
96
+ content: string,
97
+ options?: ConduitTopicPublishOptions,
98
+ ): Promise<{ messageId: string }>;
99
+
100
+ /**
101
+ * Register a real-time handler for topic messages.
102
+ *
103
+ * Optional — only implemented by `LocalTransport`.
104
+ *
105
+ * @param topicName - Topic name to watch.
106
+ * @param handler - Handler invoked for each new message.
107
+ * @returns Unsubscribe function.
108
+ * @task T1252
109
+ */
110
+ onTopic?(topicName: string, handler: (message: ConduitMessage) => void): ConduitUnsubscribe;
111
+
112
+ /**
113
+ * Unsubscribe agent from a named topic.
114
+ *
115
+ * Optional — only implemented by `LocalTransport`.
116
+ *
117
+ * @param topicName - Topic name to leave.
118
+ * @task T1252
119
+ */
120
+ unsubscribeTopic?(topicName: string): Promise<void>;
64
121
  }
65
122
 
66
123
  // ============================================================================