@cleocode/cleo-os 2026.4.99 → 2026.4.101

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,359 @@
1
+ /**
2
+ * Pi binary wrapper — low-level launcher for the Pi coding agent CLI.
3
+ *
4
+ * Handles prompt delivery, process spawning, stdin/stdout/stderr wiring,
5
+ * and SIGTERM-then-SIGKILL cleanup. This module is the only place in
6
+ * cleo-os that touches `child_process.spawn` directly for Pi.
7
+ *
8
+ * @remarks
9
+ * Pi receives its prompt via **file mode**: the prompt text is written to a
10
+ * temporary file under `/tmp/` and passed as a positional CLI argument
11
+ * (`pi <file>`). This mirrors the pattern used by `PiSpawnProvider` in
12
+ * `packages/adapters/` and avoids stdin complexity for non-interactive runs.
13
+ *
14
+ * CleoOS extension injection follows the same mechanism as `cli.ts`: each
15
+ * discovered extension is prepended as `--extension <path>` so Pi loads
16
+ * bridges (CANT bridge, hooks bridge, etc.) even in non-interactive mode.
17
+ *
18
+ * Environment variable overrides (all optional):
19
+ * - `CLEO_PI_BINARY` — path to the `pi` binary (default: `pi` from PATH)
20
+ * - `CLEO_TERMINATE_GRACE_MS` — SIGTERM grace window in ms (default: `5000`)
21
+ * - `CLEO_HARNESS_OUTPUT_BUFFER` — ring buffer capacity per process (default: `500`)
22
+ *
23
+ * OpenClaw patterns adopted:
24
+ * - Extension injection via `--extension <path>` flags.
25
+ * - Prompt written to `/tmp/` with a unique suffix; cleaned up on exit.
26
+ * - `PI_TELEMETRY=0` injected by default to suppress telemetry in CI.
27
+ *
28
+ * @packageDocumentation
29
+ */
30
+ import { spawn } from 'node:child_process';
31
+ import { existsSync } from 'node:fs';
32
+ import { unlink, writeFile } from 'node:fs/promises';
33
+ import { dirname, join } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+ /** Maximum number of output lines retained in the ring buffer per process. */
36
+ const DEFAULT_OUTPUT_BUFFER_SIZE = 500;
37
+ /** Default SIGTERM → SIGKILL grace window in milliseconds. */
38
+ const DEFAULT_TERMINATE_GRACE_MS = 5000;
39
+ const __dirname = dirname(fileURLToPath(import.meta.url));
40
+ // ---------------------------------------------------------------------------
41
+ // Environment helpers
42
+ // ---------------------------------------------------------------------------
43
+ /**
44
+ * Resolve the `pi` binary path.
45
+ *
46
+ * Honours `CLEO_PI_BINARY` env var when set (absolute path or bare name),
47
+ * otherwise defaults to `pi` (expects the binary on PATH).
48
+ *
49
+ * @returns Resolved binary path string.
50
+ *
51
+ * @public
52
+ */
53
+ export function getPiBinaryPath() {
54
+ return process.env['CLEO_PI_BINARY'] ?? 'pi';
55
+ }
56
+ /**
57
+ * Return the SIGTERM grace window in milliseconds.
58
+ *
59
+ * Reads `CLEO_TERMINATE_GRACE_MS` from the environment; falls back to
60
+ * {@link DEFAULT_TERMINATE_GRACE_MS} when absent or non-positive.
61
+ *
62
+ * @public
63
+ */
64
+ export function getTerminateGraceMs() {
65
+ const raw = process.env['CLEO_TERMINATE_GRACE_MS'];
66
+ if (raw !== undefined) {
67
+ const n = Number.parseInt(raw, 10);
68
+ if (Number.isFinite(n) && n > 0)
69
+ return n;
70
+ }
71
+ return DEFAULT_TERMINATE_GRACE_MS;
72
+ }
73
+ /**
74
+ * Return the output ring buffer capacity.
75
+ *
76
+ * Reads `CLEO_HARNESS_OUTPUT_BUFFER` from the environment; falls back to
77
+ * {@link DEFAULT_OUTPUT_BUFFER_SIZE} when absent or non-positive.
78
+ *
79
+ * @public
80
+ */
81
+ export function getOutputBufferSize() {
82
+ const raw = process.env['CLEO_HARNESS_OUTPUT_BUFFER'];
83
+ if (raw !== undefined) {
84
+ const n = Number.parseInt(raw, 10);
85
+ if (Number.isFinite(n) && n > 0)
86
+ return n;
87
+ }
88
+ return DEFAULT_OUTPUT_BUFFER_SIZE;
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Extension path resolution
92
+ // ---------------------------------------------------------------------------
93
+ /**
94
+ * Collect CleoOS extension paths that exist on disk.
95
+ *
96
+ * Resolves the standard extension list relative to the compiled package root
97
+ * (`extensions/` directory adjacent to `dist/`). Only paths that exist on
98
+ * disk are returned — missing extensions are skipped silently so the adapter
99
+ * degrades gracefully when extensions are not installed.
100
+ *
101
+ * Mirrors `collectExtensionPaths()` in `cli.ts` but does not depend on
102
+ * `@cleocode/core` to keep this module free of external package imports.
103
+ *
104
+ * @returns Array of absolute `.js` extension paths.
105
+ *
106
+ * @public
107
+ */
108
+ export function resolveExtensionPaths() {
109
+ // Compiled path: dist/harnesses/pi-coding-agent/ → walk up to dist/ → package root
110
+ const packageRoot = join(__dirname, '..', '..', '..');
111
+ const extensionsDir = join(packageRoot, 'extensions');
112
+ const candidates = [
113
+ 'cleo-startup.js',
114
+ 'cleo-cant-bridge.js',
115
+ 'cleo-hooks-bridge.js',
116
+ 'cleo-chatroom.js',
117
+ 'cleo-agent-monitor.js',
118
+ ];
119
+ return candidates.map((name) => join(extensionsDir, name)).filter((p) => existsSync(p));
120
+ }
121
+ // ---------------------------------------------------------------------------
122
+ // Helpers
123
+ // ---------------------------------------------------------------------------
124
+ /**
125
+ * Append a line to a bounded ring buffer, evicting the oldest entry when the
126
+ * buffer is full.
127
+ *
128
+ * @param buffer - The mutable ring buffer.
129
+ * @param line - Line to append.
130
+ * @param bufferSize - Maximum buffer capacity.
131
+ */
132
+ function appendOutputLine(buffer, line, bufferSize) {
133
+ buffer.push(line);
134
+ if (buffer.length > bufferSize) {
135
+ buffer.shift();
136
+ }
137
+ }
138
+ /**
139
+ * Delete a temporary prompt file, swallowing any errors.
140
+ *
141
+ * @param path - Absolute path to the temp file, or `null` to skip.
142
+ */
143
+ async function cleanupTmpFile(path) {
144
+ if (path === null)
145
+ return;
146
+ try {
147
+ await unlink(path);
148
+ }
149
+ catch {
150
+ // Best-effort cleanup — ignore ENOENT and other errors.
151
+ }
152
+ }
153
+ /**
154
+ * Snapshot the current state of a process entry as a {@link HarnessProcessStatus}.
155
+ *
156
+ * @param entry - The process tracking entry.
157
+ * @returns Immutable status snapshot.
158
+ *
159
+ * @public
160
+ */
161
+ export function buildStatus(entry) {
162
+ return {
163
+ instanceId: entry.instanceId,
164
+ taskId: entry.taskId,
165
+ state: entry.state,
166
+ pid: entry.pid,
167
+ startedAt: entry.startedAt,
168
+ ...(entry.endedAt !== null ? { endedAt: entry.endedAt } : {}),
169
+ ...(entry.state === 'exited' && entry.exitCode !== null ? { exitCode: entry.exitCode } : {}),
170
+ ...(entry.error !== null ? { error: entry.error } : {}),
171
+ };
172
+ }
173
+ /**
174
+ * Create a new process tracking entry for a given instance and task.
175
+ *
176
+ * The entry starts in a transitional `'failed'` state that will be
177
+ * immediately overwritten by {@link PiWrapper.start} on success.
178
+ *
179
+ * @param instanceId - Stable instance identifier.
180
+ * @param taskId - CLEO task ID.
181
+ * @param resolveExit - Promise resolve callback wired to the exit promise.
182
+ * @returns Initialised (pre-spawn) tracking entry.
183
+ *
184
+ * @public
185
+ */
186
+ export function createProcessEntry(instanceId, taskId, resolveExit) {
187
+ return {
188
+ pid: null,
189
+ child: null,
190
+ taskId,
191
+ instanceId,
192
+ startedAt: new Date().toISOString(),
193
+ state: 'failed', // overwritten by start() on success
194
+ exitCode: null,
195
+ terminatingSignal: null,
196
+ endedAt: null,
197
+ error: null,
198
+ outputBuffer: [],
199
+ resolveExit,
200
+ tmpFile: null,
201
+ killTimer: null,
202
+ };
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // PiWrapper
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * Low-level Pi process launcher.
209
+ *
210
+ * Manages the full lifecycle of Pi CLI processes: spawning via
211
+ * {@link start}, output buffering into a ring buffer, and
212
+ * SIGTERM-then-SIGKILL termination via {@link terminate}. Process state
213
+ * is tracked in a {@link PiProcessEntry} held by the caller
214
+ * ({@link PiCodingAgentAdapter}).
215
+ *
216
+ * @public
217
+ */
218
+ export class PiWrapper {
219
+ /**
220
+ * Spawn a Pi CLI process for the given task.
221
+ *
222
+ * Writes the prompt to a temporary file in `/tmp/`, builds the argument
223
+ * list (prepending `--extension <path>` for each available CleoOS
224
+ * extension), and spawns Pi as a child process. Stdout and stderr are
225
+ * line-buffered into `entry.outputBuffer`.
226
+ *
227
+ * When spawn fails (e.g. binary not found), `entry.state` is set to
228
+ * `'failed'` and `entry.resolveExit` is called before returning.
229
+ *
230
+ * @param entry - Pre-allocated process tracking entry. Mutated in place.
231
+ * @param prompt - Prompt text to deliver to Pi.
232
+ * @param cwd - Working directory for the child process.
233
+ * @param env - Environment variable overrides merged atop the current env.
234
+ * @returns The mutated `entry` (for chaining convenience).
235
+ *
236
+ * @public
237
+ */
238
+ async start(entry, prompt, cwd, env) {
239
+ const bufferSize = getOutputBufferSize();
240
+ const binaryPath = getPiBinaryPath();
241
+ // Write prompt to a temporary file (file-mode delivery).
242
+ const tmpFile = `/tmp/cleo-pi-${entry.instanceId}.txt`;
243
+ entry.tmpFile = tmpFile;
244
+ try {
245
+ await writeFile(tmpFile, prompt, 'utf-8');
246
+ }
247
+ catch (err) {
248
+ entry.state = 'failed';
249
+ entry.error = err instanceof Error ? err.message : String(err);
250
+ entry.endedAt = new Date().toISOString();
251
+ entry.resolveExit(buildStatus(entry));
252
+ return entry;
253
+ }
254
+ // Build argument list: [--extension <path>...] <promptFile>
255
+ const extensionPaths = resolveExtensionPaths();
256
+ const extensionFlags = extensionPaths.flatMap((p) => ['--extension', p]);
257
+ const args = [...extensionFlags, tmpFile];
258
+ // Merge environment: parent env → telemetry disable → caller overrides.
259
+ const parentEnv = Object.fromEntries(Object.entries(process.env).filter((pair) => pair[1] !== undefined));
260
+ const mergedEnv = {
261
+ ...parentEnv,
262
+ PI_TELEMETRY: '0', // suppress telemetry in CI/sandbox runs
263
+ ...env,
264
+ };
265
+ let child;
266
+ try {
267
+ child = spawn(binaryPath, args, {
268
+ cwd,
269
+ env: mergedEnv,
270
+ stdio: ['ignore', 'pipe', 'pipe'],
271
+ });
272
+ }
273
+ catch (err) {
274
+ entry.state = 'failed';
275
+ entry.error = err instanceof Error ? err.message : String(err);
276
+ entry.endedAt = new Date().toISOString();
277
+ await cleanupTmpFile(tmpFile);
278
+ entry.tmpFile = null;
279
+ entry.resolveExit(buildStatus(entry));
280
+ return entry;
281
+ }
282
+ entry.child = child;
283
+ entry.pid = child.pid ?? null;
284
+ entry.state = 'running';
285
+ // Buffer stdout lines into the ring buffer.
286
+ child.stdout?.setEncoding('utf-8');
287
+ child.stdout?.on('data', (chunk) => {
288
+ for (const rawLine of chunk.split('\n')) {
289
+ const line = rawLine.trimEnd();
290
+ if (line.length === 0)
291
+ continue;
292
+ appendOutputLine(entry.outputBuffer, { source: 'stdout', line, timestamp: new Date().toISOString() }, bufferSize);
293
+ }
294
+ });
295
+ // Buffer stderr lines into the ring buffer.
296
+ child.stderr?.setEncoding('utf-8');
297
+ child.stderr?.on('data', (chunk) => {
298
+ for (const rawLine of chunk.split('\n')) {
299
+ const line = rawLine.trimEnd();
300
+ if (line.length === 0)
301
+ continue;
302
+ appendOutputLine(entry.outputBuffer, { source: 'stderr', line, timestamp: new Date().toISOString() }, bufferSize);
303
+ }
304
+ });
305
+ // Handle process exit.
306
+ child.on('close', async (code, signal) => {
307
+ if (entry.killTimer !== null) {
308
+ clearTimeout(entry.killTimer);
309
+ entry.killTimer = null;
310
+ }
311
+ entry.child = null;
312
+ entry.endedAt = new Date().toISOString();
313
+ if (entry.state === 'running') {
314
+ entry.state = signal !== null ? 'killed' : 'exited';
315
+ entry.exitCode = code;
316
+ entry.terminatingSignal = signal;
317
+ }
318
+ await cleanupTmpFile(entry.tmpFile);
319
+ entry.tmpFile = null;
320
+ entry.resolveExit(buildStatus(entry));
321
+ });
322
+ return entry;
323
+ }
324
+ /**
325
+ * Terminate a Pi process via SIGTERM-then-SIGKILL.
326
+ *
327
+ * Sends SIGTERM immediately. If the process has not exited within the
328
+ * configured grace window ({@link getTerminateGraceMs}), sends SIGKILL.
329
+ * Idempotent — subsequent calls when the process is already dead are no-ops.
330
+ *
331
+ * @param entry - The process tracking entry to terminate.
332
+ *
333
+ * @public
334
+ */
335
+ terminate(entry) {
336
+ if (entry.child === null || entry.state !== 'running')
337
+ return;
338
+ const graceMs = getTerminateGraceMs();
339
+ entry.state = 'killed';
340
+ try {
341
+ entry.child.kill('SIGTERM');
342
+ }
343
+ catch {
344
+ // Process may have already exited between the state check and the signal.
345
+ }
346
+ entry.killTimer = setTimeout(() => {
347
+ entry.killTimer = null;
348
+ if (entry.child !== null) {
349
+ try {
350
+ entry.child.kill('SIGKILL');
351
+ }
352
+ catch {
353
+ // Already dead — ignore.
354
+ }
355
+ }
356
+ }, graceMs);
357
+ }
358
+ }
359
+ //# sourceMappingURL=pi-wrapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi-wrapper.js","sourceRoot":"","sources":["../../../src/harnesses/pi-coding-agent/pi-wrapper.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,185 @@
1
+ /**
2
+ * HarnessAdapter type contract for cleo-os sandbox harnesses.
3
+ *
4
+ * Defines the minimal surface a cleo-os harness adapter must expose for
5
+ * controlling a process lifecycle: spawn, status, kill, and output streaming.
6
+ * This interface is intentionally simpler than the full CAAMP
7
+ * {@link packages/caamp | Harness} contract — cleo-os adapters wrap an
8
+ * external agent binary rather than a first-class CAAMP provider.
9
+ *
10
+ * @remarks
11
+ * cleo-os harness adapters follow the OpenClaw sandbox pattern: a thin
12
+ * TypeScript wrapper that controls an external binary (Pi, Claude Code, etc.)
13
+ * inside either a host-native or Docker sandbox environment.
14
+ *
15
+ * @see ADR-049 — CleoOS Sovereignty Invariants
16
+ * @see ADR-050 — CleoOS Sovereign Harness: Distribution Binding Charter
17
+ * @packageDocumentation
18
+ */
19
+ /**
20
+ * Lifecycle state of a spawned harness process.
21
+ *
22
+ * - `'running'` — process was started and has not yet exited.
23
+ * - `'exited'` — process exited normally (exit code may be non-zero).
24
+ * - `'killed'` — process was terminated via {@link HarnessAdapter.kill}.
25
+ * - `'failed'` — process could not be started (pre-spawn error).
26
+ *
27
+ * @public
28
+ */
29
+ export type HarnessProcessState = 'running' | 'exited' | 'killed' | 'failed';
30
+ /**
31
+ * Describes a running or completed harness process.
32
+ *
33
+ * @public
34
+ */
35
+ export interface HarnessProcessStatus {
36
+ /** Stable identifier for this process instance. */
37
+ instanceId: string;
38
+ /** Task ID associated with this process. */
39
+ taskId: string;
40
+ /** Current lifecycle state. */
41
+ state: HarnessProcessState;
42
+ /** OS process ID, or `null` when the process could not be started. */
43
+ pid: number | null;
44
+ /** ISO-8601 timestamp when the process was spawned. */
45
+ startedAt: string;
46
+ /**
47
+ * ISO-8601 timestamp when the process ended.
48
+ * Only populated when `state` is `'exited'` or `'killed'`.
49
+ * @defaultValue undefined
50
+ */
51
+ endedAt?: string;
52
+ /**
53
+ * Exit code from the process.
54
+ * Only populated when `state` is `'exited'`.
55
+ * @defaultValue undefined
56
+ */
57
+ exitCode?: number | null;
58
+ /**
59
+ * Error message when `state` is `'failed'`.
60
+ * @defaultValue undefined
61
+ */
62
+ error?: string;
63
+ }
64
+ /**
65
+ * A single line of output emitted from a harness process.
66
+ *
67
+ * @public
68
+ */
69
+ export interface HarnessOutputLine {
70
+ /** Source stream: stdout or stderr. */
71
+ source: 'stdout' | 'stderr';
72
+ /** The text content of this line (without trailing newline). */
73
+ line: string;
74
+ /** ISO-8601 timestamp when this line was captured. */
75
+ timestamp: string;
76
+ }
77
+ /**
78
+ * Options passed to {@link HarnessAdapter.spawn}.
79
+ *
80
+ * @public
81
+ */
82
+ export interface HarnessSpawnOptions {
83
+ /**
84
+ * Working directory for the spawned process.
85
+ * @defaultValue `process.cwd()`
86
+ */
87
+ cwd?: string;
88
+ /**
89
+ * Environment variable overrides merged atop the current process environment.
90
+ * @defaultValue undefined
91
+ */
92
+ env?: Record<string, string>;
93
+ /**
94
+ * When `true`, run the process inside a Docker sandbox container instead of
95
+ * host-native. Requires `CLEO_PI_SANDBOXED=1` or explicit `sandboxed: true`.
96
+ * @defaultValue false
97
+ */
98
+ sandboxed?: boolean;
99
+ /**
100
+ * Abort signal. When it fires, the adapter terminates the process via
101
+ * SIGTERM-then-SIGKILL with the configured grace window.
102
+ * @defaultValue undefined
103
+ */
104
+ signal?: AbortSignal;
105
+ }
106
+ /**
107
+ * Result returned synchronously from {@link HarnessAdapter.spawn}.
108
+ *
109
+ * @public
110
+ */
111
+ export interface HarnessSpawnResult {
112
+ /** Stable instance identifier (can be passed to status/kill/output). */
113
+ instanceId: string;
114
+ /** OS process ID, or `null` on failure. */
115
+ pid: number | null;
116
+ /** Promise resolving once the process exits or is killed. */
117
+ exitPromise: Promise<HarnessProcessStatus>;
118
+ }
119
+ /**
120
+ * Contract every cleo-os harness adapter MUST implement.
121
+ *
122
+ * @remarks
123
+ * A harness adapter wraps one external agent binary and exposes a uniform
124
+ * lifecycle surface. The adapter is responsible for:
125
+ *
126
+ * - Writing the prompt to the correct location (temp file or stdin).
127
+ * - Launching the external binary with the right flags and environment.
128
+ * - Optionally routing the launch through a Docker sandbox container
129
+ * (see {@link HarnessSpawnOptions.sandboxed}).
130
+ * - Buffering recent output lines for post-mortem diagnostics.
131
+ * - Terminating processes cleanly via SIGTERM-then-SIGKILL.
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const adapter = new PiCodingAgentAdapter();
136
+ * const { instanceId, exitPromise } = await adapter.spawn('T123', 'Implement the feature', { cwd: '/project' });
137
+ * const status = await exitPromise;
138
+ * console.log(status.exitCode);
139
+ * ```
140
+ *
141
+ * @public
142
+ */
143
+ export interface HarnessAdapter {
144
+ /** Short adapter identifier (e.g. `"pi-coding-agent"`). */
145
+ readonly id: string;
146
+ /**
147
+ * Spawn the agent binary with the given task prompt.
148
+ *
149
+ * @param taskId - Stable CLEO task identifier associated with this run.
150
+ * @param prompt - Prompt / instruction text to pass to the agent.
151
+ * @param opts - Spawn options (cwd, env, sandboxed, signal).
152
+ * @returns Spawn result containing the instance ID and exit promise.
153
+ */
154
+ spawn(taskId: string, prompt: string, opts?: HarnessSpawnOptions): Promise<HarnessSpawnResult>;
155
+ /**
156
+ * Query the current status of a spawned process.
157
+ *
158
+ * @param instanceId - Instance ID returned from {@link spawn}.
159
+ * @returns Current status, or `null` when the ID is not tracked.
160
+ */
161
+ status(instanceId: string): HarnessProcessStatus | null;
162
+ /**
163
+ * Terminate a running process.
164
+ *
165
+ * Sends SIGTERM, waits for the configured grace window (`CLEO_TERMINATE_GRACE_MS`
166
+ * or 5000 ms), then sends SIGKILL when the process has not yet exited.
167
+ * Idempotent — subsequent calls after the first are no-ops.
168
+ *
169
+ * @param instanceId - Instance ID returned from {@link spawn}.
170
+ */
171
+ kill(instanceId: string): Promise<void>;
172
+ /**
173
+ * Return recently captured output lines for a process.
174
+ *
175
+ * The adapter keeps a bounded ring buffer of the last
176
+ * `CLEO_HARNESS_OUTPUT_BUFFER` (default: 500) lines. Useful for
177
+ * post-mortem diagnostics without injecting output into the parent
178
+ * LLM context.
179
+ *
180
+ * @param instanceId - Instance ID returned from {@link spawn}.
181
+ * @returns Array of recent output lines, oldest first.
182
+ */
183
+ output(instanceId: string): HarnessOutputLine[];
184
+ }
185
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/harnesses/pi-coding-agent/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE7E;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,KAAK,EAAE,mBAAmB,CAAC;IAC3B,sEAAsE;IACtE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,uCAAuC;IACvC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAC5B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,6DAA6D;IAC7D,WAAW,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC5C;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAEpB;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAE/F;;;;;OAKG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAAC;IAExD;;;;;;;;OAQG;IACH,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAAC;CACjD"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * HarnessAdapter type contract for cleo-os sandbox harnesses.
3
+ *
4
+ * Defines the minimal surface a cleo-os harness adapter must expose for
5
+ * controlling a process lifecycle: spawn, status, kill, and output streaming.
6
+ * This interface is intentionally simpler than the full CAAMP
7
+ * {@link packages/caamp | Harness} contract — cleo-os adapters wrap an
8
+ * external agent binary rather than a first-class CAAMP provider.
9
+ *
10
+ * @remarks
11
+ * cleo-os harness adapters follow the OpenClaw sandbox pattern: a thin
12
+ * TypeScript wrapper that controls an external binary (Pi, Claude Code, etc.)
13
+ * inside either a host-native or Docker sandbox environment.
14
+ *
15
+ * @see ADR-049 — CleoOS Sovereignty Invariants
16
+ * @see ADR-050 — CleoOS Sovereign Harness: Distribution Binding Charter
17
+ * @packageDocumentation
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/harnesses/pi-coding-agent/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/cleo-os",
3
- "version": "2026.4.99",
3
+ "version": "2026.4.101",
4
4
  "description": "CleoOS — the batteries-included agentic development environment wrapping Pi",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
@@ -13,9 +13,9 @@
13
13
  "@mariozechner/pi-coding-agent": ">=0.60.0",
14
14
  "@sinclair/typebox": "^0.34.49",
15
15
  "env-paths": "^4.0.0",
16
- "@cleocode/cant": "2026.4.99",
17
- "@cleocode/cleo": "2026.4.99",
18
- "@cleocode/core": "2026.4.99"
16
+ "@cleocode/cant": "2026.4.101",
17
+ "@cleocode/cleo": "2026.4.101",
18
+ "@cleocode/core": "2026.4.101"
19
19
  },
20
20
  "devDependencies": {
21
21
  "typescript": "^6.0.2",