@kernel.chat/kbot 3.99.20 → 3.99.21

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,66 @@
1
+ /**
2
+ * kbot-control-client.ts — TCP client for kbot-control.amxd
3
+ *
4
+ * Singleton client that connects to the kbot-control Max for Live device
5
+ * at 127.0.0.1:9000. Newline-delimited JSON-RPC 2.0 over plain TCP.
6
+ * Zero npm dependencies — uses node:net only.
7
+ *
8
+ * Supersedes:
9
+ * - ableton-osc.ts (UDP 11000, AbletonOSC Remote Script)
10
+ * - ableton-bridge.ts (TCP 9001, AbletonBridge)
11
+ * - ableton-m4l.ts (TCP 9999, kbot-bridge.amxd)
12
+ *
13
+ * Fallback order when this client can't connect:
14
+ * 1. kbot-control.amxd (this)
15
+ * 2. ableton-osc.ts (legacy)
16
+ * 3. Claude computer-use MCP (last resort)
17
+ */
18
+ export interface RpcRequest {
19
+ jsonrpc: '2.0';
20
+ id: number;
21
+ method: string;
22
+ params?: Record<string, unknown>;
23
+ }
24
+ export interface RpcResponse<T = unknown> {
25
+ jsonrpc: '2.0';
26
+ id: number | null;
27
+ result?: T;
28
+ error?: {
29
+ code: number;
30
+ message: string;
31
+ };
32
+ }
33
+ export type Listener = (value: unknown) => void;
34
+ export declare class KbotControlClient {
35
+ private static instance;
36
+ private socket;
37
+ private connected;
38
+ private buffer;
39
+ private pending;
40
+ private listeners;
41
+ private nextId;
42
+ private connectAttempt;
43
+ static HOST: string;
44
+ static PORT: number;
45
+ static TIMEOUT: number;
46
+ static CONNECT_TIMEOUT: number;
47
+ private constructor();
48
+ static get(): KbotControlClient;
49
+ connect(): Promise<void>;
50
+ private handleData;
51
+ private handleMessage;
52
+ call<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
53
+ subscribe(path: string, fn: Listener): Promise<void>;
54
+ unsubscribe(path: string, fn: Listener): Promise<void>;
55
+ private pollers;
56
+ private startPolling;
57
+ private stopPolling;
58
+ disconnect(): void;
59
+ get isConnected(): boolean;
60
+ }
61
+ /**
62
+ * Convenience: connect + call + return result.
63
+ * Throws if kbot-control.amxd isn't loaded in Ableton.
64
+ */
65
+ export declare function kc<T = unknown>(method: string, params?: Record<string, unknown>): Promise<T>;
66
+ //# sourceMappingURL=kbot-control-client.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * kbot-control-client.ts — TCP client for kbot-control.amxd
3
+ *
4
+ * Singleton client that connects to the kbot-control Max for Live device
5
+ * at 127.0.0.1:9000. Newline-delimited JSON-RPC 2.0 over plain TCP.
6
+ * Zero npm dependencies — uses node:net only.
7
+ *
8
+ * Supersedes:
9
+ * - ableton-osc.ts (UDP 11000, AbletonOSC Remote Script)
10
+ * - ableton-bridge.ts (TCP 9001, AbletonBridge)
11
+ * - ableton-m4l.ts (TCP 9999, kbot-bridge.amxd)
12
+ *
13
+ * Fallback order when this client can't connect:
14
+ * 1. kbot-control.amxd (this)
15
+ * 2. ableton-osc.ts (legacy)
16
+ * 3. Claude computer-use MCP (last resort)
17
+ */
18
+ import * as net from 'node:net';
19
+ export class KbotControlClient {
20
+ static instance = null;
21
+ socket = null;
22
+ connected = false;
23
+ buffer = '';
24
+ pending = new Map();
25
+ listeners = new Map();
26
+ nextId = 1;
27
+ connectAttempt = null;
28
+ static HOST = '127.0.0.1';
29
+ static PORT = 9000;
30
+ static TIMEOUT = 15_000;
31
+ static CONNECT_TIMEOUT = 3_000;
32
+ constructor() { }
33
+ static get() {
34
+ if (!this.instance)
35
+ this.instance = new KbotControlClient();
36
+ return this.instance;
37
+ }
38
+ async connect() {
39
+ if (this.connected)
40
+ return;
41
+ if (this.connectAttempt)
42
+ return this.connectAttempt;
43
+ this.connectAttempt = new Promise((resolve, reject) => {
44
+ const sock = new net.Socket();
45
+ const timer = setTimeout(() => {
46
+ sock.destroy();
47
+ reject(new Error(`kbot-control: connect timeout (${KbotControlClient.CONNECT_TIMEOUT}ms)`));
48
+ }, KbotControlClient.CONNECT_TIMEOUT);
49
+ sock.connect(KbotControlClient.PORT, KbotControlClient.HOST, () => {
50
+ clearTimeout(timer);
51
+ this.socket = sock;
52
+ this.connected = true;
53
+ resolve();
54
+ });
55
+ sock.on('data', (chunk) => this.handleData(chunk.toString()));
56
+ sock.on('close', () => {
57
+ this.connected = false;
58
+ this.socket = null;
59
+ for (const [, p] of this.pending) {
60
+ clearTimeout(p.timer);
61
+ p.reject(new Error('kbot-control: connection closed'));
62
+ }
63
+ this.pending.clear();
64
+ });
65
+ sock.on('error', (e) => {
66
+ clearTimeout(timer);
67
+ this.connected = false;
68
+ reject(new Error(`kbot-control: ${e.message} — is kbot-control.amxd loaded in Ableton on a track?`));
69
+ });
70
+ });
71
+ try {
72
+ await this.connectAttempt;
73
+ }
74
+ finally {
75
+ this.connectAttempt = null;
76
+ }
77
+ }
78
+ handleData(chunk) {
79
+ this.buffer += chunk;
80
+ const lines = this.buffer.split('\n');
81
+ this.buffer = lines.pop() || '';
82
+ for (const line of lines) {
83
+ const trimmed = line.trim();
84
+ if (!trimmed)
85
+ continue;
86
+ this.handleMessage(trimmed);
87
+ }
88
+ }
89
+ handleMessage(raw) {
90
+ let msg;
91
+ try {
92
+ msg = JSON.parse(raw);
93
+ }
94
+ catch {
95
+ return;
96
+ }
97
+ // Notifications (listener events) — no id
98
+ if ('method' in msg && msg.method === 'notify') {
99
+ const { path, value } = msg.params || {};
100
+ if (path) {
101
+ const set = this.listeners.get(path);
102
+ if (set)
103
+ for (const fn of set)
104
+ fn(value);
105
+ }
106
+ return;
107
+ }
108
+ // Server hello greeting
109
+ if ('method' in msg && msg.method === 'hello')
110
+ return;
111
+ const response = msg;
112
+ if (response.id == null)
113
+ return;
114
+ const p = this.pending.get(response.id);
115
+ if (!p)
116
+ return;
117
+ this.pending.delete(response.id);
118
+ clearTimeout(p.timer);
119
+ if (response.error)
120
+ p.reject(new Error(`[${response.error.code}] ${response.error.message}`));
121
+ else
122
+ p.resolve(response.result);
123
+ }
124
+ async call(method, params) {
125
+ await this.connect();
126
+ if (!this.socket)
127
+ throw new Error('kbot-control: not connected');
128
+ const id = this.nextId++;
129
+ const req = { jsonrpc: '2.0', id, method, params };
130
+ return new Promise((resolve, reject) => {
131
+ const timer = setTimeout(() => {
132
+ this.pending.delete(id);
133
+ reject(new Error(`kbot-control: timeout on ${method} (${KbotControlClient.TIMEOUT}ms)`));
134
+ }, KbotControlClient.TIMEOUT);
135
+ this.pending.set(id, {
136
+ resolve: (r) => resolve(r),
137
+ reject,
138
+ timer,
139
+ });
140
+ this.socket.write(JSON.stringify(req) + '\n');
141
+ });
142
+ }
143
+ async subscribe(path, fn) {
144
+ let set = this.listeners.get(path);
145
+ if (!set) {
146
+ set = new Set();
147
+ this.listeners.set(path, set);
148
+ await this.call('listen.subscribe', { path });
149
+ // Start a polling loop as a fallback until the outlet-based push works.
150
+ // This also catches events even when the push path breaks.
151
+ this.startPolling(path);
152
+ }
153
+ set.add(fn);
154
+ }
155
+ async unsubscribe(path, fn) {
156
+ const set = this.listeners.get(path);
157
+ if (!set)
158
+ return;
159
+ set.delete(fn);
160
+ if (set.size === 0) {
161
+ this.listeners.delete(path);
162
+ this.stopPolling(path);
163
+ try {
164
+ await this.call('listen.unsubscribe', { path });
165
+ }
166
+ catch { /* ignore */ }
167
+ }
168
+ }
169
+ pollers = new Map();
170
+ startPolling(path, intervalMs = 150) {
171
+ if (this.pollers.has(path))
172
+ return;
173
+ const state = { timer: null, since: 0 };
174
+ state.timer = setInterval(async () => {
175
+ try {
176
+ const r = await this.call('listen.poll', { path, since: state.since });
177
+ if (r && r.events && r.events.length > 0) {
178
+ state.since = r.latest_seq;
179
+ const set = this.listeners.get(path);
180
+ if (set) {
181
+ for (const ev of r.events) {
182
+ // LiveAPI often reports values as [propertyName, value]; unwrap.
183
+ let v = ev.value;
184
+ if (Array.isArray(v) && v.length === 2 && typeof v[0] === 'string')
185
+ v = v[1];
186
+ for (const fn of set)
187
+ fn(v);
188
+ }
189
+ }
190
+ }
191
+ else if (r && typeof r.latest_seq === 'number') {
192
+ state.since = r.latest_seq;
193
+ }
194
+ }
195
+ catch { /* ignore transient errors */ }
196
+ }, intervalMs);
197
+ this.pollers.set(path, state);
198
+ }
199
+ stopPolling(path) {
200
+ const s = this.pollers.get(path);
201
+ if (s) {
202
+ clearInterval(s.timer);
203
+ this.pollers.delete(path);
204
+ }
205
+ }
206
+ disconnect() {
207
+ for (const [, s] of this.pollers)
208
+ clearInterval(s.timer);
209
+ this.pollers.clear();
210
+ if (this.socket)
211
+ this.socket.destroy();
212
+ this.socket = null;
213
+ this.connected = false;
214
+ }
215
+ get isConnected() { return this.connected; }
216
+ }
217
+ /**
218
+ * Convenience: connect + call + return result.
219
+ * Throws if kbot-control.amxd isn't loaded in Ableton.
220
+ */
221
+ export async function kc(method, params) {
222
+ return KbotControlClient.get().call(method, params);
223
+ }
224
+ //# sourceMappingURL=kbot-control-client.js.map
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Outcome classification for tool calls.
3
+ * Required for training an action-token model — cannot be backfilled from logs.
4
+ */
5
+ export type ToolOutcome = 'success' | 'error' | 'timeout' | 'empty' | 'large';
1
6
  export interface ObservedToolCall {
2
7
  ts: string;
3
8
  tool: string;
@@ -5,6 +10,14 @@ export interface ObservedToolCall {
5
10
  result_length?: number;
6
11
  session?: string;
7
12
  error?: boolean;
13
+ /** Schema version. Absent = v1 (legacy). 2 = includes durationMs/outcome/resultSize. */
14
+ schema?: number;
15
+ /** Wall-clock duration of tool execution in milliseconds. (schema v2+) */
16
+ durationMs?: number;
17
+ /** Outcome classification for training. (schema v2+) */
18
+ outcome?: ToolOutcome;
19
+ /** Bytes of serialized result (Buffer.byteLength of result string). (schema v2+) */
20
+ resultSize?: number;
8
21
  }
9
22
  export interface ObserverStats {
10
23
  totalObserved: number;
package/dist/observer.js CHANGED
@@ -11,8 +11,12 @@
11
11
  // The log file is written by a Claude Code hook (PostToolUse) that appends
12
12
  // one JSON line per tool call to ~/.kbot/observer/session.jsonl
13
13
  //
14
- // Format per line:
14
+ // Format per line (schema v1 — legacy):
15
15
  // {"ts":"ISO","tool":"Read","args":{"file_path":"/src/foo.ts"},"result_length":1234,"session":"abc"}
16
+ //
17
+ // Format per line (schema v2 — includes action-token training fields):
18
+ // {"schema":2,"ts":"ISO","tool":"Read","args":{...},"result_length":1234,"session":"abc",
19
+ // "durationMs":42,"outcome":"success","resultSize":1234,"error":false}
16
20
  import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs';
17
21
  import { join } from 'node:path';
18
22
  import { homedir } from 'node:os';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Hierarchical Planner — persistence helpers.
3
+ *
4
+ * Goals are stored as individual JSON files under
5
+ * `~/.kbot/planner/goals/<id>.json`. The currently-active goal id is recorded
6
+ * at `~/.kbot/planner/active.json` as `{ "goalId": "<ulid>" }`.
7
+ *
8
+ * Scope: atomic read/write, listing, and active-pointer management. No
9
+ * tier logic here.
10
+ */
11
+ import type { SessionGoal } from './types.js';
12
+ /** Default on-disk root: `~/.kbot/planner/`. */
13
+ export declare function defaultStateDir(): string;
14
+ /** Read a single goal by id, or null if missing. */
15
+ export declare function readGoal(stateDir: string, id: string): Promise<SessionGoal | null>;
16
+ /** Write (create-or-overwrite) a goal file. */
17
+ export declare function writeGoal(stateDir: string, goal: SessionGoal): Promise<void>;
18
+ /** List every goal on disk (unsorted). */
19
+ export declare function listGoals(stateDir: string): Promise<SessionGoal[]>;
20
+ /** Set the active goal pointer. The goal must already exist on disk. */
21
+ export declare function setActive(stateDir: string, goalId: string): Promise<void>;
22
+ /** Read the active goal (resolves pointer → goal file). Returns null if unset. */
23
+ export declare function getActive(stateDir: string): Promise<SessionGoal | null>;
24
+ /** Clear the active pointer (goal files untouched). */
25
+ export declare function clearActive(stateDir: string): Promise<void>;
26
+ //# sourceMappingURL=persistence.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Hierarchical Planner — persistence helpers.
3
+ *
4
+ * Goals are stored as individual JSON files under
5
+ * `~/.kbot/planner/goals/<id>.json`. The currently-active goal id is recorded
6
+ * at `~/.kbot/planner/active.json` as `{ "goalId": "<ulid>" }`.
7
+ *
8
+ * Scope: atomic read/write, listing, and active-pointer management. No
9
+ * tier logic here.
10
+ */
11
+ import { promises as fs } from 'node:fs';
12
+ import * as os from 'node:os';
13
+ import * as path from 'node:path';
14
+ /** Default on-disk root: `~/.kbot/planner/`. */
15
+ export function defaultStateDir() {
16
+ return path.join(os.homedir(), '.kbot', 'planner');
17
+ }
18
+ function goalsDir(stateDir) {
19
+ return path.join(stateDir, 'goals');
20
+ }
21
+ function goalPath(stateDir, id) {
22
+ return path.join(goalsDir(stateDir), `${id}.json`);
23
+ }
24
+ function activePath(stateDir) {
25
+ return path.join(stateDir, 'active.json');
26
+ }
27
+ async function ensureDir(dir) {
28
+ await fs.mkdir(dir, { recursive: true });
29
+ }
30
+ /** Read a single goal by id, or null if missing. */
31
+ export async function readGoal(stateDir, id) {
32
+ try {
33
+ const raw = await fs.readFile(goalPath(stateDir, id), 'utf8');
34
+ return JSON.parse(raw);
35
+ }
36
+ catch (err) {
37
+ if (err.code === 'ENOENT')
38
+ return null;
39
+ throw err;
40
+ }
41
+ }
42
+ /** Write (create-or-overwrite) a goal file. */
43
+ export async function writeGoal(stateDir, goal) {
44
+ await ensureDir(goalsDir(stateDir));
45
+ const tmp = goalPath(stateDir, goal.id) + '.tmp';
46
+ const final = goalPath(stateDir, goal.id);
47
+ await fs.writeFile(tmp, JSON.stringify(goal, null, 2), 'utf8');
48
+ await fs.rename(tmp, final);
49
+ }
50
+ /** List every goal on disk (unsorted). */
51
+ export async function listGoals(stateDir) {
52
+ const dir = goalsDir(stateDir);
53
+ let entries;
54
+ try {
55
+ entries = await fs.readdir(dir);
56
+ }
57
+ catch (err) {
58
+ if (err.code === 'ENOENT')
59
+ return [];
60
+ throw err;
61
+ }
62
+ const goals = [];
63
+ for (const entry of entries) {
64
+ if (!entry.endsWith('.json'))
65
+ continue;
66
+ try {
67
+ const raw = await fs.readFile(path.join(dir, entry), 'utf8');
68
+ goals.push(JSON.parse(raw));
69
+ }
70
+ catch {
71
+ // skip unreadable / malformed files; don't let one bad file poison the list
72
+ }
73
+ }
74
+ return goals;
75
+ }
76
+ /** Set the active goal pointer. The goal must already exist on disk. */
77
+ export async function setActive(stateDir, goalId) {
78
+ const existing = await readGoal(stateDir, goalId);
79
+ if (!existing) {
80
+ throw new Error(`setActive: goal ${goalId} not found in ${goalsDir(stateDir)}`);
81
+ }
82
+ await ensureDir(stateDir);
83
+ const tmp = activePath(stateDir) + '.tmp';
84
+ await fs.writeFile(tmp, JSON.stringify({ goalId }, null, 2), 'utf8');
85
+ await fs.rename(tmp, activePath(stateDir));
86
+ }
87
+ /** Read the active goal (resolves pointer → goal file). Returns null if unset. */
88
+ export async function getActive(stateDir) {
89
+ let ptr;
90
+ try {
91
+ const raw = await fs.readFile(activePath(stateDir), 'utf8');
92
+ ptr = JSON.parse(raw);
93
+ }
94
+ catch (err) {
95
+ if (err.code === 'ENOENT')
96
+ return null;
97
+ throw err;
98
+ }
99
+ if (!ptr.goalId)
100
+ return null;
101
+ return readGoal(stateDir, ptr.goalId);
102
+ }
103
+ /** Clear the active pointer (goal files untouched). */
104
+ export async function clearActive(stateDir) {
105
+ try {
106
+ await fs.unlink(activePath(stateDir));
107
+ }
108
+ catch (err) {
109
+ if (err.code !== 'ENOENT')
110
+ throw err;
111
+ }
112
+ }
113
+ //# sourceMappingURL=persistence.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * HierarchicalPlanner — Phase 1 passthrough.
3
+ *
4
+ * This file lays the pipe for the four-tier hierarchical planner described in
5
+ * ./types.ts. Today it does NOT implement tiers — `planAndExecute` delegates
6
+ * the entire user turn to the existing flat `autonomousExecute` from
7
+ * ../../planner.ts and synthesizes a `PlannerResult` shell around it.
8
+ *
9
+ * Real goal persistence is live: `loadGoal`, `createGoal`, and `setGoal` read
10
+ * and write `~/.kbot/planner/goals/<id>.json` plus the `active.json` pointer.
11
+ * The current PlannerResult.goal is always `null` in this phase because the
12
+ * tier-1 selection/creation loop isn't wired in yet — callers opt into goal
13
+ * tracking explicitly via createGoal/setGoal.
14
+ *
15
+ * Feature flag: `planAndExecute` throws unless `process.env.KBOT_PLANNER ===
16
+ * 'hierarchical'`. Nothing in production calls this class yet.
17
+ */
18
+ import type { AgentOptions } from '../../agent.js';
19
+ import type { Action, Phase, SessionGoal, TurnMetrics } from './types.js';
20
+ export type Ulid = string;
21
+ /** Minimal shape planAndExecute needs from the caller. Expand in Phase 2. */
22
+ export interface SessionContext {
23
+ /** Optional session identifier (e.g. from memory.ts). */
24
+ sessionId?: string;
25
+ /** Agent options to thread into the underlying planner. */
26
+ agentOpts: AgentOptions;
27
+ /** When true, skip interactive approval on the underlying planner. */
28
+ autoApprove?: boolean;
29
+ }
30
+ /**
31
+ * Phase-1 `PlannerResult`. Shape matches the spec in this ticket — NOT the
32
+ * richer `PlannerResult` in ./types.ts, which targets Phase 2+. We use a local
33
+ * name to avoid a clash and to flag this is transitional.
34
+ */
35
+ export interface PlannerResult {
36
+ /** Always null in Phase 1; Phase 2 will populate via tier-1 logic. */
37
+ goal: SessionGoal | null;
38
+ /** Ephemeral placeholder phase spanning just this turn. */
39
+ phase: Phase;
40
+ /** The action derived from autonomousExecute's flat plan. */
41
+ action: Action;
42
+ metrics: TurnMetrics;
43
+ }
44
+ export interface HierarchicalPlannerOptions {
45
+ /** Override persistence root; defaults to `~/.kbot/planner/`. */
46
+ stateDir?: string;
47
+ }
48
+ export declare class HierarchicalPlanner {
49
+ private readonly stateDir;
50
+ constructor(opts?: HierarchicalPlannerOptions);
51
+ /**
52
+ * Plan and execute one user turn.
53
+ *
54
+ * Phase 1: feature-flag gated passthrough to autonomousExecute.
55
+ * Phase 2+: replace the body with the Tier 1–4 cascade.
56
+ */
57
+ planAndExecute(userTurn: string, ctx: SessionContext): Promise<PlannerResult>;
58
+ /** Read the currently-active goal from disk, or null if none is set. */
59
+ loadGoal(): Promise<SessionGoal | null>;
60
+ /**
61
+ * Create a new goal on disk and mark it active.
62
+ * Fields the caller omits are filled with sensible defaults.
63
+ */
64
+ createGoal(spec?: Partial<SessionGoal>): Promise<SessionGoal>;
65
+ /** Activate an existing goal by id. Throws if the goal does not exist. */
66
+ setGoal(goalId: Ulid): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=session-planner.d.ts.map
@@ -0,0 +1,141 @@
1
+ /**
2
+ * HierarchicalPlanner — Phase 1 passthrough.
3
+ *
4
+ * This file lays the pipe for the four-tier hierarchical planner described in
5
+ * ./types.ts. Today it does NOT implement tiers — `planAndExecute` delegates
6
+ * the entire user turn to the existing flat `autonomousExecute` from
7
+ * ../../planner.ts and synthesizes a `PlannerResult` shell around it.
8
+ *
9
+ * Real goal persistence is live: `loadGoal`, `createGoal`, and `setGoal` read
10
+ * and write `~/.kbot/planner/goals/<id>.json` plus the `active.json` pointer.
11
+ * The current PlannerResult.goal is always `null` in this phase because the
12
+ * tier-1 selection/creation loop isn't wired in yet — callers opt into goal
13
+ * tracking explicitly via createGoal/setGoal.
14
+ *
15
+ * Feature flag: `planAndExecute` throws unless `process.env.KBOT_PLANNER ===
16
+ * 'hierarchical'`. Nothing in production calls this class yet.
17
+ */
18
+ import { randomBytes } from 'node:crypto';
19
+ import { autonomousExecute } from '../../planner.js';
20
+ import { defaultStateDir, getActive, readGoal, setActive, writeGoal, } from './persistence.js';
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // ULID-ish id generator.
23
+ // We depend on `ulid` if it's resolvable at runtime, otherwise fall back to a
24
+ // 26-char Crockford-base32 string sourced from crypto.randomBytes. Either way
25
+ // IDs are sortable-ish and collision-resistant enough for per-goal filenames.
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+ const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
28
+ function cryptoUlid() {
29
+ // 10 chars of timestamp (ms) + 16 chars of randomness = 26 chars.
30
+ let ts = Date.now();
31
+ const tsChars = [];
32
+ for (let i = 0; i < 10; i++) {
33
+ tsChars.unshift(CROCKFORD[ts % 32]);
34
+ ts = Math.floor(ts / 32);
35
+ }
36
+ const bytes = randomBytes(10);
37
+ const rand = [];
38
+ // 80 bits → 16 base32 chars. Pull 5 bits at a time.
39
+ let acc = 0;
40
+ let accBits = 0;
41
+ for (const b of bytes) {
42
+ acc = (acc << 8) | b;
43
+ accBits += 8;
44
+ while (accBits >= 5) {
45
+ accBits -= 5;
46
+ rand.push(CROCKFORD[(acc >> accBits) & 0x1f]);
47
+ }
48
+ }
49
+ return tsChars.join('') + rand.slice(0, 16).join('');
50
+ }
51
+ export class HierarchicalPlanner {
52
+ stateDir;
53
+ constructor(opts = {}) {
54
+ this.stateDir = opts.stateDir ?? defaultStateDir();
55
+ }
56
+ /**
57
+ * Plan and execute one user turn.
58
+ *
59
+ * Phase 1: feature-flag gated passthrough to autonomousExecute.
60
+ * Phase 2+: replace the body with the Tier 1–4 cascade.
61
+ */
62
+ async planAndExecute(userTurn, ctx) {
63
+ const flag = process.env.KBOT_PLANNER;
64
+ if (flag !== 'hierarchical') {
65
+ throw new Error(`HierarchicalPlanner.planAndExecute is gated behind KBOT_PLANNER=hierarchical ` +
66
+ `(got ${flag ?? 'unset'}). This path is Phase-1 scaffolding only.`);
67
+ }
68
+ const startedAt = Date.now();
69
+ const plan = await autonomousExecute(userTurn, ctx.agentOpts, {
70
+ autoApprove: ctx.autoApprove ?? true,
71
+ });
72
+ const phase = {
73
+ id: cryptoUlid(),
74
+ goalId: '', // no goal linked in Phase 1
75
+ kind: 'other',
76
+ objective: userTurn,
77
+ exitCriteria: [],
78
+ startedAt: new Date(startedAt).toISOString(),
79
+ endedAt: new Date().toISOString(),
80
+ status: plan.status === 'completed' ? 'done' : 'active',
81
+ };
82
+ const steps = plan.steps.map(step => ({ ...step }));
83
+ const action = {
84
+ id: cryptoUlid(),
85
+ phaseId: phase.id,
86
+ userTurn,
87
+ summary: plan.summary,
88
+ steps,
89
+ createdAt: plan.createdAt,
90
+ status: plan.status === 'completed'
91
+ ? 'done'
92
+ : plan.status === 'failed'
93
+ ? 'failed'
94
+ : 'running',
95
+ };
96
+ const metrics = {
97
+ tier1Calls: 0,
98
+ tier2Calls: 0,
99
+ tier3Calls: 1, // autonomousExecute counts as one tier-3 invocation
100
+ tier4Calls: steps.length,
101
+ tokensIn: 0,
102
+ tokensOut: 0,
103
+ wallMs: Date.now() - startedAt,
104
+ };
105
+ // Phase 1 never attaches a goal — that's Phase 2.
106
+ return { goal: null, phase, action, metrics };
107
+ }
108
+ /** Read the currently-active goal from disk, or null if none is set. */
109
+ async loadGoal() {
110
+ return getActive(this.stateDir);
111
+ }
112
+ /**
113
+ * Create a new goal on disk and mark it active.
114
+ * Fields the caller omits are filled with sensible defaults.
115
+ */
116
+ async createGoal(spec = {}) {
117
+ const now = new Date().toISOString();
118
+ const goal = {
119
+ id: spec.id ?? cryptoUlid(),
120
+ title: spec.title ?? '(untitled goal)',
121
+ intent: spec.intent ?? '',
122
+ acceptance: spec.acceptance ?? [],
123
+ createdAt: spec.createdAt ?? now,
124
+ updatedAt: spec.updatedAt ?? now,
125
+ status: spec.status ?? 'active',
126
+ tags: spec.tags,
127
+ };
128
+ await writeGoal(this.stateDir, goal);
129
+ await setActive(this.stateDir, goal.id);
130
+ return goal;
131
+ }
132
+ /** Activate an existing goal by id. Throws if the goal does not exist. */
133
+ async setGoal(goalId) {
134
+ const existing = await readGoal(this.stateDir, goalId);
135
+ if (!existing) {
136
+ throw new Error(`setGoal: goal ${goalId} not found under ${this.stateDir}`);
137
+ }
138
+ await setActive(this.stateDir, goalId);
139
+ }
140
+ }
141
+ //# sourceMappingURL=session-planner.js.map