@oh-my-pi/pi-coding-agent 8.4.3 → 8.5.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [8.5.0] - 2026-01-27
6
+
7
+ ### Added
8
+ - Added subagent support for preloading skill contents into the system prompt instead of listing available skills
9
+ - Added session init entries to capture system prompt, task, tools, and output schema for subagent session logs
10
+
11
+ ### Fixed
12
+ - Reduced Task tool progress update overhead to keep the UI responsive during high-volume streaming output
13
+ - Fixed subagent session logs dropping pre-assistant entries (user/task metadata) before the first assistant response
14
+
15
+ ### Removed
16
+ - Removed enter-plan-mode tool
17
+ ## [8.4.5] - 2026-01-26
18
+
19
+ ### Added
20
+ - Model usage tracking to record and retrieve most recently used models
21
+ - Model sorting in selector based on usage history
22
+
23
+ ### Changed
24
+ - Renamed `head_limit` parameter to `limit` in grep and find tools for consistency
25
+ - Added `context` as an alias for the `c` context parameter in grep tool
26
+ - Made hidden files inclusion configurable in find tool via `hidden` parameter (defaults to true)
27
+ - Added support for reading ignore patterns from .gitignore and .ignore files in find tool
28
+
29
+ ### Fixed
30
+ - Respected .gitignore rules when filtering find tool results by glob pattern
5
31
  ## [8.4.2] - 2026-01-25
6
32
 
7
33
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.4.3",
3
+ "version": "8.5.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -83,11 +83,11 @@
83
83
  "test": "bun test"
84
84
  },
85
85
  "dependencies": {
86
- "@oh-my-pi/omp-stats": "8.4.3",
87
- "@oh-my-pi/pi-agent-core": "8.4.3",
88
- "@oh-my-pi/pi-ai": "8.4.3",
89
- "@oh-my-pi/pi-tui": "8.4.3",
90
- "@oh-my-pi/pi-utils": "8.4.3",
86
+ "@oh-my-pi/omp-stats": "8.5.0",
87
+ "@oh-my-pi/pi-agent-core": "8.5.0",
88
+ "@oh-my-pi/pi-ai": "8.5.0",
89
+ "@oh-my-pi/pi-tui": "8.5.0",
90
+ "@oh-my-pi/pi-utils": "8.5.0",
91
91
  "@openai/agents": "^0.4.3",
92
92
  "@sinclair/typebox": "^0.34.46",
93
93
  "ajv": "^8.17.1",
package/src/cursor.ts CHANGED
@@ -170,7 +170,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
170
170
  context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
171
171
  ignore_case: args.caseInsensitive || undefined,
172
172
  type: args.type || undefined,
173
- head_limit: args.headLimit ?? undefined,
173
+ limit: args.headLimit ?? undefined,
174
174
  multiline: args.multiline || undefined,
175
175
  });
176
176
  return toolResultMessage;
@@ -75,7 +75,6 @@ export class ModelSelectorComponent extends Container {
75
75
  private allModels: ModelItem[] = [];
76
76
  private filteredModels: ModelItem[] = [];
77
77
  private selectedIndex: number = 0;
78
- private currentModel?: Model<any>;
79
78
  private defaultModel?: Model<any>;
80
79
  private smolModel?: Model<any>;
81
80
  private slowModel?: Model<any>;
@@ -98,7 +97,7 @@ export class ModelSelectorComponent extends Container {
98
97
 
99
98
  constructor(
100
99
  tui: TUI,
101
- currentModel: Model<any> | undefined,
100
+ _currentModel: Model<any> | undefined,
102
101
  settingsManager: SettingsManager,
103
102
  modelRegistry: ModelRegistry,
104
103
  scopedModels: ReadonlyArray<ScopedModelItem>,
@@ -109,7 +108,6 @@ export class ModelSelectorComponent extends Container {
109
108
  super();
110
109
 
111
110
  this.tui = tui;
112
- this.currentModel = currentModel;
113
111
  this.settingsManager = settingsManager;
114
112
  this.modelRegistry = modelRegistry;
115
113
  this.scopedModels = scopedModels;
@@ -213,6 +211,44 @@ export class ModelSelectorComponent extends Container {
213
211
  }
214
212
  }
215
213
 
214
+ private sortModels(models: ModelItem[]): void {
215
+ // Sort: tagged models (default/smol/slow) first, then MRU, then alphabetical
216
+ const mruOrder = this.settingsManager.getStorage()?.getModelUsageOrder() ?? [];
217
+ const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
218
+
219
+ models.sort((a, b) => {
220
+ const aKey = `${a.provider}/${a.id}`;
221
+ const bKey = `${b.provider}/${b.id}`;
222
+
223
+ // Tagged models first: default (0), smol (1), slow (2), untagged (3)
224
+ const aTag = modelsAreEqual(this.defaultModel, a.model)
225
+ ? 0
226
+ : modelsAreEqual(this.smolModel, a.model)
227
+ ? 1
228
+ : modelsAreEqual(this.slowModel, a.model)
229
+ ? 2
230
+ : 3;
231
+ const bTag = modelsAreEqual(this.defaultModel, b.model)
232
+ ? 0
233
+ : modelsAreEqual(this.smolModel, b.model)
234
+ ? 1
235
+ : modelsAreEqual(this.slowModel, b.model)
236
+ ? 2
237
+ : 3;
238
+ if (aTag !== bTag) return aTag - bTag;
239
+
240
+ // Then MRU order (models in mruIndex come before those not in it)
241
+ const aMru = mruIndex.get(aKey) ?? Number.MAX_SAFE_INTEGER;
242
+ const bMru = mruIndex.get(bKey) ?? Number.MAX_SAFE_INTEGER;
243
+ if (aMru !== bMru) return aMru - bMru;
244
+
245
+ // Finally alphabetical by provider, then id
246
+ const providerCmp = a.provider.localeCompare(b.provider);
247
+ if (providerCmp !== 0) return providerCmp;
248
+ return a.id.localeCompare(b.id);
249
+ });
250
+ }
251
+
216
252
  private async loadModels(): Promise<void> {
217
253
  let models: ModelItem[];
218
254
 
@@ -249,16 +285,7 @@ export class ModelSelectorComponent extends Container {
249
285
  }
250
286
  }
251
287
 
252
- // Sort: current model first, then by provider, then by id
253
- models.sort((a, b) => {
254
- const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
255
- const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
256
- if (aIsCurrent && !bIsCurrent) return -1;
257
- if (!aIsCurrent && bIsCurrent) return 1;
258
- const providerCmp = a.provider.localeCompare(b.provider);
259
- if (providerCmp !== 0) return providerCmp;
260
- return a.id.localeCompare(b.id);
261
- });
288
+ this.sortModels(models);
262
289
 
263
290
  this.allModels = models;
264
291
  this.filteredModels = models;
@@ -315,7 +342,9 @@ export class ModelSelectorComponent extends Container {
315
342
  this.updateTabBar();
316
343
  baseModels = this.allModels;
317
344
  }
318
- this.filteredModels = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
345
+ const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
346
+ this.sortModels(fuzzyMatches);
347
+ this.filteredModels = fuzzyMatches;
319
348
  } else {
320
349
  this.filteredModels = baseModels;
321
350
  }
@@ -558,9 +558,7 @@ export class ToolExecutionComponent extends Container {
558
558
  const context: Record<string, unknown> = {};
559
559
  const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
560
560
  if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
561
- let timeoutSec = value > 1000 ? value / 1000 : value;
562
- timeoutSec = Math.max(1, Math.min(maxSeconds, timeoutSec));
563
- return timeoutSec;
561
+ return Math.max(1, Math.min(maxSeconds, value));
564
562
  };
565
563
 
566
564
  if (this.toolName === "bash" && this.result) {
@@ -247,12 +247,6 @@ export class EventController {
247
247
  this.ctx.setTodos(details.todos);
248
248
  }
249
249
  }
250
- if (event.toolName === "enter_plan_mode" && !event.isError) {
251
- const details = event.result.details as import("../../tools").EnterPlanModeDetails | undefined;
252
- if (details) {
253
- await this.ctx.handleEnterPlanModeTool(details);
254
- }
255
- }
256
250
  if (event.toolName === "exit_plan_mode" && !event.isError) {
257
251
  const details = event.result.details as ExitPlanModeDetails | undefined;
258
252
  if (details) {
@@ -29,7 +29,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
29
29
  import { HistoryStorage } from "../session/history-storage";
30
30
  import type { SessionContext, SessionManager } from "../session/session-manager";
31
31
  import { getRecentSessions } from "../session/session-manager";
32
- import type { EnterPlanModeDetails, ExitPlanModeDetails } from "../tools";
32
+ import type { ExitPlanModeDetails } from "../tools";
33
33
  import { setTerminalTitle } from "../utils/title-generator";
34
34
  import type { AssistantMessageComponent } from "./components/assistant-message";
35
35
  import type { BashExecutionComponent } from "./components/bash-execution";
@@ -631,25 +631,6 @@ export class InteractiveMode implements InteractiveModeContext {
631
631
  await this.enterPlanMode();
632
632
  }
633
633
 
634
- async handleEnterPlanModeTool(details: EnterPlanModeDetails): Promise<void> {
635
- if (this.planModeEnabled) {
636
- this.showWarning("Plan mode is already active.");
637
- return;
638
- }
639
-
640
- const confirmed = await this.showHookConfirm(
641
- "Enter plan mode?",
642
- "This enables read-only planning and creates a plan file for approval.",
643
- );
644
- if (!confirmed) {
645
- return;
646
- }
647
-
648
- const planFilePath = details.planFilePath || this.getPlanFilePath();
649
- this.planModePlanFilePath = planFilePath;
650
- await this.enterPlanMode({ planFilePath, workflow: details.workflow });
651
- }
652
-
653
634
  async handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void> {
654
635
  if (!this.planModeEnabled) {
655
636
  this.showWarning("Plan mode is not active.");
@@ -178,7 +178,6 @@ export interface InteractiveModeContext {
178
178
  openExternalEditor(): void;
179
179
  registerExtensionShortcuts(): void;
180
180
  handlePlanModeCommand(): Promise<void>;
181
- handleEnterPlanModeTool(details: import("../tools").EnterPlanModeDetails): Promise<void>;
182
181
  handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
183
182
 
184
183
  // Hook UI methods
@@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description.
43
43
  {{/list}}
44
44
  </available_skills>
45
45
  {{/if}}
46
+ {{#if preloadedSkills.length}}
47
+ The following skills are preloaded in full. Apply their instructions directly.
48
+
49
+ <preloaded_skills>
50
+ {{#list preloadedSkills join="\n"}}
51
+ <skill name="{{name}}">
52
+ <location>skill://{{escapeXml name}}</location>
53
+ <content>
54
+ {{content}}
55
+ </content>
56
+ </skill>
57
+ {{/list}}
58
+ </preloaded_skills>
59
+ {{/if}}
46
60
  {{#if rules.length}}
47
61
  The following rules define project-specific guidelines and constraints:
48
62
 
@@ -19,6 +19,10 @@ Create your plan at `{{planFilePath}}`.
19
19
 
20
20
  The plan file is the ONLY file you may write or edit.
21
21
 
22
+ <important>
23
+ Plan execution runs in a fresh context (session cleared). Make the plan file self-contained: include any requirements, decisions, key findings, and remaining todos needed to continue without prior session history.
24
+ </important>
25
+
22
26
  {{#if reentry}}
23
27
  ## Re-entry
24
28
 
@@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding.
264
264
  {{/list}}
265
265
  </skills>
266
266
  {{/if}}
267
+ {{#if preloadedSkills.length}}
268
+ <preloaded_skills>
269
+ The following skills are preloaded in full. Apply their instructions directly.
270
+
271
+ {{#list preloadedSkills join="\n"}}
272
+ <skill name="{{name}}">
273
+ <location>skill://{{escapeXml name}}</location>
274
+ {{content}}
275
+ </skill>
276
+ {{/list}}
277
+ </preloaded_skills>
278
+ {{/if}}
267
279
  {{#if rules.length}}
268
280
  <rules>
269
281
  Rules are local constraints.
@@ -3,12 +3,13 @@
3
3
  Fast file pattern matching that works with any codebase size.
4
4
 
5
5
  <instruction>
6
- - Supports glob patterns like "**/*.js" or "src/**/*.ts"
6
+ - Supports glob patterns like `**/*.js` or `src/**/*.ts`
7
+ - Includes hidden files by default (use `hidden: false` to exclude)
7
8
  - Speculatively perform multiple searches in parallel when potentially useful
8
9
  </instruction>
9
10
 
10
11
  <output>
11
- Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB.
12
+ Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB (configurable via `limit`).
12
13
  </output>
13
14
 
14
15
  <avoid>
@@ -16,7 +16,7 @@ Results depend on `output_mode`:
16
16
  - `count`: Match counts per file
17
17
 
18
18
  In `content` mode, truncated at 100 matches by default (configurable via `limit`).
19
- For `files_with_matches` and `count` modes, use `head_limit` to truncate results.
19
+ For `files_with_matches` and `count` modes, use `limit` to truncate results.
20
20
  </output>
21
21
 
22
22
  <critical>
@@ -38,6 +38,7 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
38
38
  - `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
39
39
  - `description`: Short human-readable description of what the task does
40
40
  - `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
41
+ - `skills`: (optional) Array of skill names to preload into this task's system prompt. When set, the skills index section is omitted and the full SKILL.md contents are embedded.
41
42
  - `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
42
43
  </parameters>
43
44
 
package/src/sdk.ts CHANGED
@@ -156,6 +156,8 @@ export interface CreateAgentSessionOptions {
156
156
 
157
157
  /** Skills. Default: discovered from multiple locations */
158
158
  skills?: Skill[];
159
+ /** Skills to inline into the system prompt instead of listing available skills. */
160
+ preloadedSkills?: Skill[];
159
161
  /** Context files (AGENTS.md content). Default: discovered walking up from cwd */
160
162
  contextFiles?: Array<{ path: string; content: string }>;
161
163
  /** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
@@ -993,6 +995,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
993
995
  const defaultPrompt = await buildSystemPromptInternal({
994
996
  cwd,
995
997
  skills,
998
+ preloadedSkills: options.preloadedSkills,
996
999
  contextFiles,
997
1000
  tools,
998
1001
  toolNames,
@@ -1007,6 +1010,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1007
1010
  return await buildSystemPromptInternal({
1008
1011
  cwd,
1009
1012
  skills,
1013
+ preloadedSkills: options.preloadedSkills,
1010
1014
  contextFiles,
1011
1015
  tools,
1012
1016
  toolNames,
@@ -1676,6 +1676,7 @@ export class AgentSession {
1676
1676
  this.agent.setModel(model);
1677
1677
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
1678
1678
  this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
1679
+ this.settingsManager.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
1679
1680
 
1680
1681
  // Re-clamp thinking level for new model's capabilities
1681
1682
  this.setThinkingLevel(this.thinkingLevel);
@@ -1694,6 +1695,7 @@ export class AgentSession {
1694
1695
 
1695
1696
  this.agent.setModel(model);
1696
1697
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
1698
+ this.settingsManager.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
1697
1699
 
1698
1700
  // Re-clamp thinking level for new model's capabilities
1699
1701
  this.setThinkingLevel(this.thinkingLevel);
@@ -1790,6 +1792,7 @@ export class AgentSession {
1790
1792
  this.agent.setModel(next.model);
1791
1793
  this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
1792
1794
  this.settingsManager.setModelRole("default", `${next.model.provider}/${next.model.id}`);
1795
+ this.settingsManager.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
1793
1796
 
1794
1797
  // Apply thinking level (setThinkingLevel clamps to model capabilities)
1795
1798
  this.setThinkingLevel(next.thinkingLevel);
@@ -1817,6 +1820,7 @@ export class AgentSession {
1817
1820
  this.agent.setModel(nextModel);
1818
1821
  this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
1819
1822
  this.settingsManager.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
1823
+ this.settingsManager.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
1820
1824
 
1821
1825
  // Re-clamp thinking level for new model's capabilities
1822
1826
  this.setThinkingLevel(this.thinkingLevel);
@@ -23,6 +23,12 @@ type AuthRow = {
23
23
  data: string;
24
24
  };
25
25
 
26
+ /** Row shape for model_usage table queries */
27
+ type ModelUsageRow = {
28
+ model_key: string;
29
+ last_used_at: number;
30
+ };
31
+
26
32
  /**
27
33
  * Auth credential with database row ID for updates/deletes.
28
34
  * Wraps AuthCredential with storage metadata.
@@ -34,7 +40,7 @@ export interface StoredAuthCredential {
34
40
  }
35
41
 
36
42
  /** Bump when schema changes require migration */
37
- const SCHEMA_VERSION = 2;
43
+ const SCHEMA_VERSION = 3;
38
44
 
39
45
  /**
40
46
  * Type guard for plain objects.
@@ -126,6 +132,9 @@ export class AgentStorage {
126
132
  private deleteAuthStmt: Statement;
127
133
  private deleteAuthByProviderStmt: Statement;
128
134
  private countAuthStmt: Statement;
135
+ private upsertModelUsageStmt: Statement;
136
+ private listModelUsageStmt: Statement;
137
+ private modelUsageCache: string[] | null = null;
129
138
 
130
139
  private constructor(dbPath: string) {
131
140
  this.ensureDir(dbPath);
@@ -157,6 +166,13 @@ export class AgentStorage {
157
166
  this.deleteAuthStmt = this.db.prepare("DELETE FROM auth_credentials WHERE id = ?");
158
167
  this.deleteAuthByProviderStmt = this.db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
159
168
  this.countAuthStmt = this.db.prepare("SELECT COUNT(*) as count FROM auth_credentials");
169
+
170
+ this.upsertModelUsageStmt = this.db.prepare(
171
+ "INSERT INTO model_usage (model_key, last_used_at) VALUES (?, unixepoch()) ON CONFLICT(model_key) DO UPDATE SET last_used_at = unixepoch()",
172
+ );
173
+ this.listModelUsageStmt = this.db.prepare(
174
+ "SELECT model_key, last_used_at FROM model_usage ORDER BY last_used_at DESC",
175
+ );
160
176
  }
161
177
 
162
178
  /**
@@ -186,6 +202,11 @@ CREATE TABLE IF NOT EXISTS cache (
186
202
  );
187
203
  CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
188
204
 
205
+ CREATE TABLE IF NOT EXISTS model_usage (
206
+ model_key TEXT PRIMARY KEY,
207
+ last_used_at INTEGER NOT NULL DEFAULT (unixepoch())
208
+ );
209
+
189
210
  CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
190
211
  `);
191
212
 
@@ -357,6 +378,38 @@ CREATE TABLE settings (
357
378
  }
358
379
  }
359
380
 
381
+ /**
382
+ * Records model usage, updating the last-used timestamp.
383
+ * @param modelKey - Model key in "provider/modelId" format
384
+ */
385
+ recordModelUsage(modelKey: string): void {
386
+ try {
387
+ this.upsertModelUsageStmt.run(modelKey);
388
+ this.modelUsageCache = null;
389
+ } catch (error) {
390
+ logger.warn("AgentStorage failed to record model usage", { modelKey, error: String(error) });
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Gets model keys ordered by most recently used.
396
+ * Results are cached until recordModelUsage is called.
397
+ * @returns Array of model keys ("provider/modelId") in MRU order
398
+ */
399
+ getModelUsageOrder(): string[] {
400
+ if (this.modelUsageCache) {
401
+ return this.modelUsageCache;
402
+ }
403
+ try {
404
+ const rows = this.listModelUsageStmt.all() as ModelUsageRow[];
405
+ this.modelUsageCache = rows.map(row => row.model_key);
406
+ return this.modelUsageCache;
407
+ } catch (error) {
408
+ logger.warn("AgentStorage failed to get model usage order", { error: String(error) });
409
+ return [];
410
+ }
411
+ }
412
+
360
413
  /**
361
414
  * Checks if any auth credentials exist in storage.
362
415
  * @returns True if at least one credential is stored
@@ -110,6 +110,19 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
110
110
  injectedRules: string[];
111
111
  }
112
112
 
113
+ /** Session init entry - captures initial context for subagent sessions (debugging/replay). */
114
+ export interface SessionInitEntry extends SessionEntryBase {
115
+ type: "session_init";
116
+ /** Full system prompt sent to the model */
117
+ systemPrompt: string;
118
+ /** Initial task/user message */
119
+ task: string;
120
+ /** Tools available to the agent */
121
+ tools: string[];
122
+ /** Output schema if structured output was requested */
123
+ outputSchema?: unknown;
124
+ }
125
+
113
126
  /**
114
127
  * Custom message entry for extensions to inject messages into LLM context.
115
128
  * Use customType to identify your extension's entries.
@@ -140,7 +153,8 @@ export type SessionEntry =
140
153
  | CustomEntry
141
154
  | CustomMessageEntry
142
155
  | LabelEntry
143
- | TtsrInjectionEntry;
156
+ | TtsrInjectionEntry
157
+ | SessionInitEntry;
144
158
 
145
159
  /** Raw file entry (includes header) */
146
160
  export type FileEntry = SessionHeader | SessionEntry;
@@ -1263,7 +1277,7 @@ export class SessionManager {
1263
1277
  if (this.persistError) throw this.persistError;
1264
1278
 
1265
1279
  const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
1266
- if (!hasAssistant) return;
1280
+ if (!hasAssistant && !this.flushed) return;
1267
1281
 
1268
1282
  if (!this.flushed) {
1269
1283
  this.flushed = true;
@@ -1368,6 +1382,19 @@ export class SessionManager {
1368
1382
  return entry.id;
1369
1383
  }
1370
1384
 
1385
+ /** Append session init metadata (for subagent debugging/replay). Returns entry id. */
1386
+ appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
1387
+ const entry: SessionInitEntry = {
1388
+ type: "session_init",
1389
+ id: generateId(this.byId),
1390
+ parentId: this.leafId,
1391
+ timestamp: new Date().toISOString(),
1392
+ ...init,
1393
+ };
1394
+ this._appendEntry(entry);
1395
+ return entry.id;
1396
+ }
1397
+
1371
1398
  /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
1372
1399
  appendCompaction<T = unknown>(
1373
1400
  summary: string,
@@ -23,6 +23,24 @@ interface GitContext {
23
23
  commits: string;
24
24
  }
25
25
 
26
+ type PreloadedSkill = { name: string; content: string };
27
+
28
+ async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
29
+ const contents = await Promise.all(
30
+ preloadedSkills.map(async skill => {
31
+ try {
32
+ const content = await Bun.file(skill.filePath).text();
33
+ return { name: skill.name, content };
34
+ } catch (err) {
35
+ const message = err instanceof Error ? err.message : String(err);
36
+ throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
37
+ }
38
+ }),
39
+ );
40
+
41
+ return contents;
42
+ }
43
+
26
44
  /**
27
45
  * Load git context for the system prompt.
28
46
  * Returns structured git data or null if not in a git repo.
@@ -643,6 +661,8 @@ export interface BuildSystemPromptOptions {
643
661
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
644
662
  /** Pre-loaded skills (skips discovery if provided). */
645
663
  skills?: Skill[];
664
+ /** Skills to inline into the system prompt instead of listing available skills. */
665
+ preloadedSkills?: Skill[];
646
666
  /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
647
667
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
648
668
  }
@@ -662,6 +682,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
662
682
  cwd,
663
683
  contextFiles: providedContextFiles,
664
684
  skills: providedSkills,
685
+ preloadedSkills: providedPreloadedSkills,
665
686
  rules,
666
687
  } = options;
667
688
  const resolvedCwd = cwd ?? process.cwd();
@@ -707,13 +728,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
707
728
  const skills =
708
729
  providedSkills ??
709
730
  (skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
731
+ const preloadedSkills = providedPreloadedSkills;
732
+ const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
710
733
 
711
734
  // Get git context
712
735
  const git = await loadGitContext(resolvedCwd);
713
736
 
714
737
  // Filter skills to only include those with read tool
715
738
  const hasRead = tools?.has("read");
716
- const filteredSkills = hasRead ? skills : [];
739
+ const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
717
740
 
718
741
  if (resolvedCustomPrompt) {
719
742
  return renderPromptTemplate(customSystemPromptTemplate, {
@@ -724,6 +747,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
724
747
  agentsMdSearch,
725
748
  git,
726
749
  skills: filteredSkills,
750
+ preloadedSkills: preloadedSkillContents,
727
751
  rules: rules ?? [],
728
752
  dateTime,
729
753
  cwd: resolvedCwd,
@@ -738,6 +762,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
738
762
  agentsMdSearch,
739
763
  git,
740
764
  skills: filteredSkills,
765
+ preloadedSkills: preloadedSkillContents,
741
766
  rules: rules ?? [],
742
767
  dateTime,
743
768
  cwd: resolvedCwd,