pi-hermes-memory 0.7.10 → 0.7.12

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.
@@ -1,17 +1,5 @@
1
1
  import { DatabaseManager } from './db.js';
2
-
3
- /**
4
- * Escape a string for FTS5 query syntax.
5
- * Wraps the query in double quotes to treat it as a literal phrase.
6
- */
7
- function escapeFts5Query(query: string): string {
8
- // If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
9
- if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
10
- return query;
11
- }
12
- // Otherwise, wrap in double quotes to treat as literal phrase
13
- return `"${query.replace(/"/g, '""')}"`;
14
- }
2
+ import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
15
3
 
16
4
  /**
17
5
  * Search result from session history.
@@ -52,6 +40,10 @@ export function searchSessions(
52
40
  query: string,
53
41
  options: SessionSearchOptions = {}
54
42
  ): SessionSearchResult[] {
43
+ if (query.trim().length === 0) {
44
+ return [];
45
+ }
46
+
55
47
  const db = dbManager.getDb();
56
48
  const { limit = 10, project, role, since } = options;
57
49
 
@@ -60,8 +52,12 @@ export function searchSessions(
60
52
  const params: unknown[] = [];
61
53
 
62
54
  // FTS5 match condition — use subquery for reliable rowid matching
55
+ const normalizedQuery = normalizeFts5Query(query);
56
+ if (normalizedQuery.length === 0) {
57
+ return [];
58
+ }
63
59
  conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
64
- params.push(escapeFts5Query(query));
60
+ params.push(normalizedQuery);
65
61
 
66
62
  // Project filter
67
63
  if (project) {
@@ -119,8 +115,10 @@ export function searchSessions(
119
115
  snippet: row.snippet,
120
116
  }));
121
117
  } catch (err) {
122
- // FTS5 can throw on malformed queries — return empty results
123
- return [];
118
+ if (isFts5QueryError(err)) {
119
+ return [];
120
+ }
121
+ throw err;
124
122
  }
125
123
  }
126
124
 
@@ -253,6 +253,8 @@ export class SkillStore {
253
253
  }
254
254
 
255
255
  return skills.sort((a, b) => {
256
+ if (a.updated !== b.updated) return b.updated.localeCompare(a.updated);
257
+ if (a.created !== b.created) return b.created.localeCompare(a.created);
256
258
  if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
257
259
  return (a.displayName || a.name).localeCompare(b.displayName || b.name);
258
260
  });
@@ -290,7 +292,7 @@ export class SkillStore {
290
292
  if (existing) {
291
293
  return {
292
294
  success: false,
293
- error: `Skill '${slug}' already exists (${skillId}). Use 'patch' or 'edit' to update it.`,
295
+ error: `Skill '${slug}' already exists (${skillId}). Use 'patch' or 'update' to update it.`,
294
296
  conflictType: "duplicate",
295
297
  similarSkillIds: [skillId],
296
298
  suggestedAction: "patch",
@@ -303,7 +305,7 @@ export class SkillStore {
303
305
  const targetId = similarSkillIds[0];
304
306
  return {
305
307
  success: false,
306
- error: `A similar global skill already exists (${targetId}). Enhance the existing skill with new learnings/failures using 'patch' or 'edit' instead of creating a duplicate.`,
308
+ error: `A similar global skill already exists (${targetId}). Enhance the existing skill with new learnings/failures using 'patch' or 'update' instead of creating a duplicate.`,
307
309
  conflictType: "similar",
308
310
  similarSkillIds,
309
311
  suggestedAction: "patch",
@@ -315,7 +317,7 @@ export class SkillStore {
315
317
  const targetId = collidingNameSkillIds[0];
316
318
  return {
317
319
  success: false,
318
- error: `A near-name global skill already exists (${targetId}) but with different intent. Use a clearer differentiated name for the new skill, or patch/edit the existing skill if the intent is actually the same.`,
320
+ error: `A near-name global skill already exists (${targetId}) but with different intent. Use a clearer differentiated name for the new skill, or patch/update the existing skill if the intent is actually the same.`,
319
321
  conflictType: "name-collision",
320
322
  similarSkillIds: collidingNameSkillIds,
321
323
  suggestedAction: "rename",
@@ -790,6 +792,8 @@ export class SkillStore {
790
792
  name: doc.name,
791
793
  displayName: doc.displayName,
792
794
  description: doc.description,
795
+ created: doc.created,
796
+ updated: doc.updated,
793
797
  };
794
798
  }
795
799
 
@@ -1,4 +1,5 @@
1
1
  import { DatabaseManager } from './db.js';
2
+ import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
2
3
  import type { MemoryCategory } from '../types.js';
3
4
 
4
5
  const MEMORY_SELECT_COLUMNS = `
@@ -550,19 +551,6 @@ export function removeExactSyncedMemories(
550
551
  };
551
552
  }
552
553
 
553
- /**
554
- * Escape a string for FTS5 query syntax.
555
- * Wraps the query in double quotes to treat it as a literal phrase.
556
- */
557
- function escapeFts5Query(query: string): string {
558
- // If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
559
- if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
560
- return query;
561
- }
562
- // Otherwise, wrap in double quotes to treat as literal phrase
563
- return `"${query.replace(/"/g, '""')}"`;
564
- }
565
-
566
554
  /**
567
555
  * Search memories using FTS5.
568
556
  */
@@ -571,6 +559,10 @@ export function searchMemories(
571
559
  query: string,
572
560
  options: { project?: string; target?: string; category?: MemoryCategory; limit?: number } = {}
573
561
  ): SqliteMemoryEntry[] {
562
+ if (query.trim().length === 0) {
563
+ return [];
564
+ }
565
+
574
566
  const db = dbManager.getDb();
575
567
  const { project, target, category, limit = 10 } = options;
576
568
 
@@ -578,8 +570,12 @@ export function searchMemories(
578
570
  const params: unknown[] = [];
579
571
 
580
572
  // FTS5 match via subquery with escaped query
573
+ const normalizedQuery = normalizeFts5Query(query);
574
+ if (normalizedQuery.length === 0) {
575
+ return [];
576
+ }
581
577
  conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
582
- params.push(escapeFts5Query(query));
578
+ params.push(normalizedQuery);
583
579
 
584
580
  if (project !== undefined) {
585
581
  if (project === null) {
@@ -611,20 +607,27 @@ export function searchMemories(
611
607
  `;
612
608
  params.push(limit);
613
609
 
614
- const rows = db.prepare(sql).all(...params) as Array<{
615
- id: number;
616
- project: string | null;
617
- target: string;
618
- category: string | null;
619
- content: string;
620
- failure_reason: string | null;
621
- tool_state: string | null;
622
- corrected_to: string | null;
623
- created: string;
624
- last_referenced: string;
625
- }>;
626
-
627
- return rows.map(mapRow);
610
+ try {
611
+ const rows = db.prepare(sql).all(...params) as Array<{
612
+ id: number;
613
+ project: string | null;
614
+ target: string;
615
+ category: string | null;
616
+ content: string;
617
+ failure_reason: string | null;
618
+ tool_state: string | null;
619
+ corrected_to: string | null;
620
+ created: string;
621
+ last_referenced: string;
622
+ }>;
623
+
624
+ return rows.map(mapRow);
625
+ } catch (err) {
626
+ if (isFts5QueryError(err)) {
627
+ return [];
628
+ }
629
+ throw err;
630
+ }
628
631
  }
629
632
 
630
633
  /**
@@ -9,6 +9,82 @@ import { StringEnum } from "@earendil-works/pi-ai";
9
9
  import { SkillStore } from "../store/skill-store.js";
10
10
  import { SKILL_TOOL_DESCRIPTION } from "../constants.js";
11
11
 
12
+ function normalizeTextList(value: unknown): string[] {
13
+ if (!Array.isArray(value)) return [];
14
+ return value
15
+ .filter((item): item is string => typeof item === "string")
16
+ .map((item) => item.trim())
17
+ .filter(Boolean);
18
+ }
19
+
20
+ function formatOrderedList(items: string[]): string {
21
+ return items.map((item, index) => `${index + 1}. ${item}`).join("\n");
22
+ }
23
+
24
+ function formatBulletList(items: string[], fallback: string): string {
25
+ if (items.length === 0) return `- ${fallback}`;
26
+ return items.map((item) => `- ${item}`).join("\n");
27
+ }
28
+
29
+ function buildStructuredSkillBody(
30
+ whenToUse: string,
31
+ procedureSteps: string[],
32
+ pitfalls: string[],
33
+ verificationSteps: string[],
34
+ ): string {
35
+ return [
36
+ "## When to Use",
37
+ whenToUse,
38
+ "",
39
+ "## Procedure",
40
+ formatOrderedList(procedureSteps),
41
+ "",
42
+ "## Pitfalls",
43
+ formatBulletList(pitfalls, "No notable pitfalls recorded yet."),
44
+ "",
45
+ "## Verification",
46
+ formatOrderedList(verificationSteps),
47
+ ].join("\n");
48
+ }
49
+
50
+ const SKILL_ID_PARAM = Type.String({
51
+ description: "Stable skill id for view/patch/update/delete. e.g., 'global:debug-typescript-errors' or 'project:my-repo:release-app'. Legacy alias 'edit' also accepts this field.",
52
+ });
53
+
54
+ const SKILL_TOOL_PARAMETERS = Type.Object({
55
+ action: StringEnum(["create", "view", "patch", "update", "edit", "delete"] as const, {
56
+ description: "The skill action to perform.",
57
+ }),
58
+ name: Type.Optional(Type.String({
59
+ description: "Skill name for create. e.g., 'debug-typescript-errors'.",
60
+ })),
61
+ skill_id: Type.Optional(SKILL_ID_PARAM),
62
+ description: Type.Optional(Type.String({
63
+ description: "One-line description of when to use this skill. Required for create; optional for update/edit.",
64
+ })),
65
+ scope: Type.Optional(StringEnum(["global", "project"] as const, {
66
+ description: "Required for create. Use 'global' for portable procedures and 'project' for repo-specific workflows.",
67
+ })),
68
+ section: Type.Optional(Type.String({
69
+ description: "Required for patch. Section header to patch. e.g., 'Procedure', 'Pitfalls'.",
70
+ })),
71
+ content: Type.Optional(Type.String({
72
+ description: "Raw markdown body for create/update/edit, or new section content for patch. For create/update/edit you can provide this or the structured fields below.",
73
+ })),
74
+ when_to_use: Type.Optional(Type.String({
75
+ description: "Structured create/update/edit field. Explain when this skill should be used and where its boundaries are.",
76
+ })),
77
+ procedure_steps: Type.Optional(Type.Array(Type.String(), {
78
+ description: "Structured create/update/edit field. Ordered concrete steps for the workflow.",
79
+ })),
80
+ pitfalls: Type.Optional(Type.Array(Type.String(), {
81
+ description: "Structured create/update/edit field. Optional common mistakes, caveats, or failure modes to avoid.",
82
+ })),
83
+ verification_steps: Type.Optional(Type.Array(Type.String(), {
84
+ description: "Structured create/update/edit field. Concrete checks that confirm the workflow succeeded.",
85
+ })),
86
+ }, { additionalProperties: false });
87
+
12
88
  export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
13
89
  pi.registerTool({
14
90
  name: "skill",
@@ -17,33 +93,67 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
17
93
  promptSnippet: "Save or manage reusable procedures and patterns",
18
94
  promptGuidelines: [
19
95
  "Use the skill tool after completing complex tasks that required trial and error or multiple tool calls.",
20
- "Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id.",
21
- "Choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
96
+ "Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id, and 'update' for a full rewrite.",
97
+ "Scope is required on create: choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
98
+ "Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool will render valid SKILL.md sections for you.",
99
+ "Use 'view' before patching or updating when you need to inspect an existing skill.",
22
100
  "Do NOT use skills for temporary task state — only for durable, reusable procedures.",
23
101
  ],
24
- parameters: Type.Object({
25
- action: StringEnum(["create", "view", "patch", "edit", "delete"] as const),
26
- name: Type.Optional(
27
- Type.String({ description: "Skill name (for create). e.g., 'debug-typescript-errors'" })
28
- ),
29
- skill_id: Type.Optional(
30
- Type.String({ description: "Stable skill id for view/patch/edit/delete. e.g., 'global:debug-typescript-errors' or 'project:my-repo:release-app'" })
31
- ),
32
- description: Type.Optional(
33
- Type.String({ description: "One-line description of when to use this skill (for create/edit)" })
34
- ),
35
- scope: Type.Optional(
36
- StringEnum(["global", "project"] as const, { description: "Optional creation scope. Omit to let the extension classify it automatically." })
37
- ),
38
- section: Type.Optional(
39
- Type.String({ description: "Section header to patch (for patch action). e.g., 'Procedure', 'Pitfalls'" })
40
- ),
41
- content: Type.Optional(
42
- Type.String({ description: "Body content for create, new section content for patch, or new body for edit" })
43
- ),
44
- }),
102
+ parameters: SKILL_TOOL_PARAMETERS,
45
103
  async execute(toolCallId, params, signal, onUpdate, ctx) {
46
- const { action, name, skill_id, description, scope, section, content } = params;
104
+ const skillParams = params as {
105
+ action: "create" | "view" | "patch" | "update" | "edit" | "delete";
106
+ name?: string;
107
+ skill_id?: string;
108
+ description?: string;
109
+ scope?: "global" | "project";
110
+ section?: string;
111
+ content?: string;
112
+ when_to_use?: string;
113
+ procedure_steps?: unknown;
114
+ pitfalls?: unknown;
115
+ verification_steps?: unknown;
116
+ };
117
+ const {
118
+ action,
119
+ name,
120
+ skill_id,
121
+ description,
122
+ scope,
123
+ section,
124
+ content,
125
+ when_to_use,
126
+ procedure_steps,
127
+ pitfalls,
128
+ verification_steps,
129
+ } = skillParams;
130
+
131
+ const whenToUse = typeof when_to_use === "string" ? when_to_use.trim() : "";
132
+ const procedureSteps = normalizeTextList(procedure_steps);
133
+ const pitfallItems = normalizeTextList(pitfalls);
134
+ const verificationSteps = normalizeTextList(verification_steps);
135
+ const hasStructuredBody = Boolean(whenToUse) || procedureSteps.length > 0 || pitfallItems.length > 0 || verificationSteps.length > 0;
136
+
137
+ const buildBodyOrError = () => {
138
+ if (content?.trim()) return { body: content.trim() };
139
+ if (!hasStructuredBody) {
140
+ return {
141
+ error: "Either content or structured fields are required. Prefer when_to_use, procedure_steps, pitfalls, and verification_steps for create/update.",
142
+ };
143
+ }
144
+ if (!whenToUse) {
145
+ return { error: "when_to_use is required when content is omitted." };
146
+ }
147
+ if (procedureSteps.length === 0) {
148
+ return { error: "procedure_steps is required when content is omitted." };
149
+ }
150
+ if (verificationSteps.length === 0) {
151
+ return { error: "verification_steps is required when content is omitted." };
152
+ }
153
+ return {
154
+ body: buildStructuredSkillBody(whenToUse, procedureSteps, pitfallItems, verificationSteps),
155
+ };
156
+ };
47
157
 
48
158
  let result;
49
159
  switch (action) {
@@ -60,13 +170,20 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
60
170
  details: {},
61
171
  };
62
172
  }
63
- if (!content) {
173
+ const createBodyResult = buildBodyOrError();
174
+ if (!createBodyResult.body) {
64
175
  return {
65
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "content (skill body) is required for 'create' action." }) }],
176
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: createBodyResult.error }) }],
66
177
  details: {},
67
178
  };
68
179
  }
69
- result = await store.create(name, description, content, scope);
180
+ if (!scope) {
181
+ return {
182
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "scope is required for 'create' action. Use 'global' or 'project'." }) }],
183
+ details: {},
184
+ };
185
+ }
186
+ result = await store.create(name, description, createBodyResult.body, scope);
70
187
  break;
71
188
 
72
189
  case "view":
@@ -109,15 +226,33 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
109
226
  result = await store.patch(skill_id, section, content);
110
227
  break;
111
228
 
112
- case "edit":
229
+ case "update":
230
+ case "edit": {
231
+ const updateActionLabel = action === "edit" ? "edit" : "update";
113
232
  if (!skill_id) {
114
233
  return {
115
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'edit' action." }) }],
234
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `skill_id is required for '${updateActionLabel}' action.` }) }],
235
+ details: {},
236
+ };
237
+ }
238
+ const updateBodyResult = buildBodyOrError();
239
+ const nextDescription = description?.trim() || "";
240
+ const nextBody = updateBodyResult.body ?? content?.trim() ?? "";
241
+ if (!nextDescription && !nextBody) {
242
+ return {
243
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Provide description, content, or structured fields for '${updateActionLabel}'.` }) }],
244
+ details: {},
245
+ };
246
+ }
247
+ if (hasStructuredBody && !updateBodyResult.body) {
248
+ return {
249
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: updateBodyResult.error }) }],
116
250
  details: {},
117
251
  };
118
252
  }
119
- result = await store.edit(skill_id, description || "", content || "");
253
+ result = await store.edit(skill_id, nextDescription, nextBody);
120
254
  break;
255
+ }
121
256
 
122
257
  case "delete":
123
258
  if (!skill_id) {
@@ -132,7 +267,7 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
132
267
  default:
133
268
  result = {
134
269
  success: false,
135
- error: `Unknown action '${action}'. Use: create, view, patch, edit, delete`,
270
+ error: `Unknown action '${action}'. Use: create, view, patch, update, delete`,
136
271
  };
137
272
  }
138
273
 
package/src/types.ts CHANGED
@@ -126,6 +126,10 @@ export interface SkillIndex {
126
126
  displayName?: string;
127
127
  /** Short description shown in skill listings */
128
128
  description: string;
129
+ /** ISO date created */
130
+ created: string;
131
+ /** ISO date last updated */
132
+ updated: string;
129
133
  }
130
134
 
131
135
  export interface SkillDocument extends SkillIndex {
@@ -133,10 +137,6 @@ export interface SkillDocument extends SkillIndex {
133
137
  body: string;
134
138
  /** Version number */
135
139
  version: number;
136
- /** ISO date created */
137
- created: string;
138
- /** ISO date last updated */
139
- updated: string;
140
140
  }
141
141
 
142
142
  export interface SkillResult {
@@ -149,7 +149,7 @@ export interface SkillResult {
149
149
  path?: string;
150
150
  conflictType?: "duplicate" | "similar" | "name-collision" | "scope-conflict";
151
151
  similarSkillIds?: string[];
152
- suggestedAction?: "patch" | "edit" | "rename";
152
+ suggestedAction?: "patch" | "update" | "rename";
153
153
  }
154
154
 
155
155
  /**
@@ -1,128 +0,0 @@
1
- /**
2
- * Skill auto-trigger — after complex tasks (8+ tool calls, 2+ distinct tool types),
3
- * trigger automatic skill extraction via pi.exec().
4
- *
5
- * This implements Hermes' "self-evaluation checkpoint" pattern.
6
- */
7
-
8
- import * as path from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
- import { MemoryStore } from "../store/memory-store.js";
12
- import { SkillStore } from "../store/skill-store.js";
13
- import { COMBINED_REVIEW_PROMPT, DEFAULT_SKILL_TRIGGER_TOOL_CALLS, ENTRY_DELIMITER } from "../constants.js";
14
- import type { MemoryConfig } from "../types.js";
15
- import { getMessageText } from "../types.js";
16
-
17
- const proceduralSkillCreatorPath = path.join(
18
- path.dirname(fileURLToPath(import.meta.url)),
19
- "..",
20
- "skills",
21
- "procedural-skill-creator",
22
- );
23
-
24
- export function setupSkillAutoTrigger(
25
- pi: ExtensionAPI,
26
- store: MemoryStore,
27
- skillStore: SkillStore,
28
- config: MemoryConfig,
29
- ): void {
30
- let triggeredThisSession = false;
31
-
32
- // Accumulate tool calls across turns (reset on trigger)
33
- let toolCallCount = 0;
34
- const toolTypes = new Set<string>();
35
-
36
- pi.on("turn_end", async (event, ctx) => {
37
- if (triggeredThisSession) return;
38
-
39
- // Count tool calls from this turn's message only (not cumulative branch scan —
40
- // otherwise the counter accumulates historical tool calls and fires prematurely).
41
- try {
42
- const msg = event.message;
43
- if (msg?.role === "assistant") {
44
- const content = msg?.content;
45
- if (Array.isArray(content)) {
46
- for (const block of content) {
47
- if (block && typeof block === "object" && block.type === "toolCall") {
48
- toolCallCount++;
49
- if ((block as { name?: string }).name) toolTypes.add((block as { name: string }).name);
50
- }
51
- }
52
- }
53
- }
54
- } catch {
55
- return;
56
- }
57
-
58
- // Require 8+ tool calls AND 2+ distinct tool types
59
- if (toolCallCount < DEFAULT_SKILL_TRIGGER_TOOL_CALLS) return;
60
- if (toolTypes.size < 2) return;
61
-
62
- triggeredThisSession = true;
63
-
64
- try {
65
- // Build conversation context
66
- const branch = ctx.sessionManager.getBranch();
67
- const parts: string[] = [];
68
-
69
- for (const entry of branch) {
70
- if (entry.type !== "message") continue;
71
- const msg = entry.message;
72
- const text = getMessageText(msg);
73
- if (!text) continue;
74
- const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
75
- parts.push(`${prefix}: ${text}`);
76
- }
77
-
78
- // Only include recent context
79
- const recentParts = parts.slice(-10);
80
-
81
- const currentMemory = store.getMemoryEntries().join(ENTRY_DELIMITER);
82
- const skillIndex = await skillStore.loadIndex();
83
- const skillSummary = skillIndex
84
- .map((skill) => `${skill.skillId} [${skill.scope}]: ${skill.displayName || skill.name} - ${skill.description}`)
85
- .join("\n");
86
-
87
- const activeProjectName = skillStore.getProjectName();
88
- const scopeLine = activeProjectName
89
- ? `Active project context: '${activeProjectName}'. If workflow details depend on this project, use scope='project'; otherwise use scope='global'.`
90
- : "No active project context detected. Use scope='global' unless the workflow is clearly project-specific.";
91
-
92
- const prompt = [
93
- "This was a complex task that required multiple tool calls. Extract any reusable procedures as skills.",
94
- "A bundled skill named procedural-skill-creator is loaded for you. Read and follow it before deciding whether to create or patch a skill.",
95
- "Always pass scope explicitly when creating a skill: scope='global' or scope='project'.",
96
- "Choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, architecture, deploy steps, or conventions.",
97
- scopeLine,
98
- "",
99
- "--- Existing Skills ---",
100
- skillSummary || "(none)",
101
- "",
102
- "--- Current Memory ---",
103
- currentMemory || "(empty)",
104
- "",
105
- "--- Recent Conversation ---",
106
- recentParts.join("\n\n"),
107
- "",
108
- "If a skill should be created, use the skill tool with action 'create'.",
109
- "If a related skill already exists, use 'patch' with its skill_id to update it.",
110
- "If nothing reusable happened, say 'Nothing to extract.' and stop.",
111
- ].join("\n");
112
-
113
- const result = await pi.exec("pi", ["-p", "--no-session", "--skill", proceduralSkillCreatorPath, prompt], {
114
- signal: ctx.signal,
115
- timeout: 60000,
116
- });
117
-
118
- if (result.code === 0 && result.stdout) {
119
- const output = result.stdout.trim();
120
- if (output && !output.toLowerCase().includes("nothing to extract")) {
121
- ctx.ui.notify("🧠 Complex task detected — skill extracted", "info");
122
- }
123
- }
124
- } catch {
125
- // Best-effort — don't block
126
- }
127
- });
128
- }