@fickydev/pigent 0.1.21 → 0.1.23

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
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.23 - 2026-05-18
4
+
5
+ ### Fixed
6
+
7
+ - Made `task-management` a runtime default active skill so task tools are available even when existing user configs do not list the skill.
8
+
9
+ ## 0.1.22 - 2026-05-18
10
+
11
+ ### Added
12
+
13
+ - Added `/skills` command to list all known skills with active/inactive flags and source labels for the current chat session.
14
+ - Added session-scoped dynamic skill loading with `/skills add <skill>`, `/skills remove <skill>`, and `/skills clear`.
15
+ - Added current skill inventory to each agent prompt so users can ask natural questions like "what skills do you have?".
16
+ - Added `agent_sessions.session_skills` persistence and migration.
17
+
3
18
  ## 0.1.21 - 2026-05-18
4
19
 
5
20
  ### Fixed
package/TODO.md CHANGED
@@ -146,6 +146,9 @@
146
146
  - [x] Make README more end-user facing
147
147
  - [x] Confirm per-agent system prompt injection
148
148
  - [x] Confirm per-agent skills loading
149
+ - [x] Add `/skills` command for current session skills with active/inactive flags
150
+ - [x] Add session-scoped dynamic skill loading via `/skills add/remove/clear`
151
+ - [x] Add skill inventory context so agents can answer natural-language skill questions
149
152
  - [x] Confirm per-agent extensions loading
150
153
  - [ ] Confirm per-agent custom tools loading
151
154
  - [x] Prototype one prompt through Pi SDK
@@ -170,6 +173,7 @@
170
173
  - [x] Add max task runs per hour rate limit
171
174
  - [x] Add `/task status` or similar command
172
175
  - [x] Add task management as agent skill + tools (natural language task CRUD)
176
+ - [x] Activate task-management by default for all sessions
173
177
  - [x] Improve task-management prompt guidance for reminder/proactive-message refusal
174
178
 
175
179
  ## Policy And Safety
@@ -0,0 +1 @@
1
+ ALTER TABLE `agent_sessions` ADD `session_skills` text DEFAULT '[]' NOT NULL;
@@ -50,6 +50,13 @@
50
50
  "when": 1779117599104,
51
51
  "tag": "0006_flippant_bruce_banner",
52
52
  "breakpoints": true
53
+ },
54
+ {
55
+ "idx": 7,
56
+ "version": "6",
57
+ "when": 1779120000000,
58
+ "tag": "0007_session_skills",
59
+ "breakpoints": true
53
60
  }
54
61
  ]
55
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fickydev/pigent",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Autonomous multi-agent daemon using Pi as core execution engine.",
@@ -1,7 +1,9 @@
1
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage } from "../channels/types";
3
3
  import type { LoadedAgentConfig } from "../config/schemas";
4
+ import { defaultActiveSkills, hasSkillRef, listBuiltInSkills, parseSessionSkills, skillDisplayName, uniqueSkillRefs } from "../config/skillRefs";
4
5
  import type { Repositories } from "../db/repositories";
6
+ import type { AgentSessionRow } from "../db/schema";
5
7
  import { logger } from "../logging/logger";
6
8
  import { PiAgentRunner } from "../pi/PiAgentRunner";
7
9
  import { createTaskTools, type TaskToolContext } from "../pi/tools/taskTools";
@@ -117,8 +119,8 @@ export class AgentRunner {
117
119
  agent,
118
120
  profile: this.registry.getProfile(agent.profile),
119
121
  session,
120
- prompt: this.composePrompt(input, chatInstructions),
121
- customTools: this.buildCustomTools(agent, input.message),
122
+ prompt: this.composePrompt(input, session, chatInstructions),
123
+ customTools: this.buildCustomTools(agent, session, input.message),
122
124
  });
123
125
 
124
126
  if (session.piSessionId !== result.piSessionId || session.piSessionPath !== result.piSessionPath) {
@@ -145,10 +147,10 @@ export class AgentRunner {
145
147
  }
146
148
  }
147
149
 
148
- private buildCustomTools(agent: LoadedAgentConfig, message: InboundMessage): ToolDefinition[] | undefined {
150
+ private buildCustomTools(agent: LoadedAgentConfig, session: AgentSessionRow, message: InboundMessage): ToolDefinition[] | undefined {
149
151
  const tools: ToolDefinition[] = [];
150
152
 
151
- if (this.hasSkill(agent, "task-management") && this.scheduler) {
153
+ if (this.hasSkill(agent, session, "task-management") && this.scheduler) {
152
154
  const ctx: TaskToolContext = {
153
155
  agentId: agent.id,
154
156
  channel: message.channel,
@@ -173,18 +175,18 @@ export class AgentRunner {
173
175
  return tools.length > 0 ? tools : undefined;
174
176
  }
175
177
 
176
- private hasSkill(agent: LoadedAgentConfig, skillName: string): boolean {
178
+ private hasSkill(agent: LoadedAgentConfig, session: AgentSessionRow, skillName: string): boolean {
177
179
  const allSkills = [
180
+ ...defaultActiveSkills(),
178
181
  ...(agent.profile ? (this.registry.getProfile(agent.profile)?.defaultSkills ?? []) : []),
179
182
  ...agent.skills,
183
+ ...parseSessionSkills(session.sessionSkills),
180
184
  ];
181
- return allSkills.some((s) => {
182
- const base = s.split("/").pop() ?? s;
183
- return base === skillName || s === skillName;
184
- });
185
+
186
+ return hasSkillRef(allSkills, skillName);
185
187
  }
186
188
 
187
- private composePrompt(input: AgentRunInput, chatInstructions: string): string {
189
+ private composePrompt(input: AgentRunInput, session: AgentSessionRow, chatInstructions: string): string {
188
190
  const parts: string[] = [
189
191
  `[Channel]\n${input.message.channel}`,
190
192
  `[Chat]\n${input.message.chatId}`,
@@ -202,11 +204,50 @@ export class AgentRunner {
202
204
  parts.push(`[Chat Instructions]\n${chatInstructions}`);
203
205
  }
204
206
 
207
+ const skillsContext = this.skillsContext(input.agentId, session);
208
+ if (skillsContext) {
209
+ parts.push(`[Skills]\n${skillsContext}`);
210
+ }
211
+
205
212
  parts.push(`[Message]\n${input.text}`);
206
213
 
207
214
  return parts.join("\n\n");
208
215
  }
209
216
 
217
+ private skillsContext(agentId: string, session: AgentSessionRow): string {
218
+ const agent = this.registry.getAgent(agentId);
219
+ if (!agent) return "";
220
+
221
+ const profile = this.registry.getProfile(agent.profile);
222
+ const defaultSkills = defaultActiveSkills();
223
+ const profileSkills = profile?.defaultSkills ?? [];
224
+ const agentSkills = agent.skills;
225
+ const sessionSkills = parseSessionSkills(session.sessionSkills);
226
+ const builtInSkills = listBuiltInSkills(import.meta.dir);
227
+ const activeSkills = uniqueSkillRefs([...defaultSkills, ...profileSkills, ...agentSkills, ...sessionSkills]);
228
+ const knownSkills = uniqueSkillRefs([...builtInSkills, ...activeSkills]).sort((a, b) =>
229
+ skillDisplayName(a).localeCompare(skillDisplayName(b)),
230
+ );
231
+
232
+ if (knownSkills.length === 0) return "No skills are configured or available.";
233
+
234
+ return knownSkills
235
+ .map((skill) => {
236
+ const name = skillDisplayName(skill);
237
+ const active = hasSkillRef(activeSkills, name);
238
+ const sources = [];
239
+
240
+ if (hasSkillRef(defaultSkills, name)) sources.push("default");
241
+ if (hasSkillRef(profileSkills, name)) sources.push("profile");
242
+ if (hasSkillRef(agentSkills, name)) sources.push("agent");
243
+ if (hasSkillRef(sessionSkills, name)) sources.push("session");
244
+ if (hasSkillRef(builtInSkills, name)) sources.push("built-in");
245
+
246
+ return `- ${name}: ${active ? "active" : "available, inactive"}${sources.length > 0 ? ` (${sources.join(", ")})` : ""}`;
247
+ })
248
+ .join("\n");
249
+ }
250
+
210
251
  private async chatInstructions(input: AgentRunInput): Promise<string> {
211
252
  if (input.message.channel !== "telegram") return "";
212
253
 
@@ -1,6 +1,7 @@
1
1
  import { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent";
2
2
  import type { InboundMessage, InlineKeyboardButton } from "../channels/types";
3
3
  import type { LoadedAgentConfig, LoadedProfileConfig, ModelChoiceConfig } from "../config/schemas";
4
+ import { defaultActiveSkills, hasSkillRef, listBuiltInSkills, parseSessionSkills, skillDisplayName, uniqueSkillRefs } from "../config/skillRefs";
4
5
  import type { AgentSessionRow } from "../db/schema";
5
6
  import type { Repositories } from "../db/repositories";
6
7
  import { PiAgentRunner, type PiContextUsage } from "../pi/PiAgentRunner";
@@ -47,6 +48,8 @@ export class BotCommandHandler {
47
48
  return { handled: true, text: await this.newSessionText(message) };
48
49
  case "/status":
49
50
  return { handled: true, text: await this.statusText(message) };
51
+ case "/skills":
52
+ return { handled: true, text: await this.skillsText(message) };
50
53
  case "/model":
51
54
  return await this.modelResult(message);
52
55
  case "/thinking":
@@ -71,6 +74,9 @@ export class BotCommandHandler {
71
74
  "/agents - list agents for this chat",
72
75
  "/new - start a fresh session for this chat",
73
76
  "/status - show current session status",
77
+ "/skills - list skills for this chat session",
78
+ "/skills add <skill> - load a skill for this session",
79
+ "/skills remove <skill> - unload a session-loaded skill",
74
80
  "/model - show model picker for this chat session",
75
81
  "/model <provider/modelId> - set model for this chat session",
76
82
  "/model default - clear model override for this chat session",
@@ -191,6 +197,84 @@ export class BotCommandHandler {
191
197
  };
192
198
  }
193
199
 
200
+ private async skillsText(message: InboundMessage): Promise<string> {
201
+ const sessionResult = await this.getDefaultSession(message);
202
+ if (!sessionResult.ok) return sessionResult.message;
203
+
204
+ const [, actionRaw, ...args] = message.text.trim().split(/\s+/);
205
+ const action = actionRaw?.toLowerCase() ?? "list";
206
+ const skillRef = args.join(" ").trim();
207
+
208
+ if (action === "add") {
209
+ if (!skillRef) return "Usage: /skills add <skill>";
210
+
211
+ const current = parseSessionSkills(sessionResult.session.sessionSkills);
212
+ const next = uniqueSkillRefs([...current, skillRef]);
213
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, next);
214
+
215
+ return [
216
+ "Skill loaded for current session: " + skillRef,
217
+ "",
218
+ "It will be included on the next agent response in this chat session.",
219
+ "Use /skills to see active skills.",
220
+ ].join("\n");
221
+ }
222
+
223
+ if (action === "remove") {
224
+ if (!skillRef) return "Usage: /skills remove <skill>";
225
+
226
+ const current = parseSessionSkills(sessionResult.session.sessionSkills);
227
+ const next = current.filter((item) => item !== skillRef);
228
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, next);
229
+
230
+ return next.length === current.length
231
+ ? "Session skill not found: " + skillRef
232
+ : "Skill removed from current session: " + skillRef;
233
+ }
234
+
235
+ if (action === "clear") {
236
+ await this.repositories.sessions.updateSessionSkills(sessionResult.session.id, []);
237
+ return "Session-loaded skills cleared.";
238
+ }
239
+
240
+ if (action !== "list") {
241
+ return [
242
+ "Usage:",
243
+ "/skills - list skills",
244
+ "/skills add <skill> - load skill for this session",
245
+ "/skills remove <skill> - unload session skill",
246
+ "/skills clear - clear session-loaded skills",
247
+ ].join("\n");
248
+ }
249
+
250
+ const agent = sessionResult.agent;
251
+ const profile = this.registry.getProfile(agent.profile);
252
+ const defaultSkills = defaultActiveSkills();
253
+ const profileSkills = profile?.defaultSkills ?? [];
254
+ const agentSkills = agent.skills;
255
+ const sessionSkills = parseSessionSkills(sessionResult.session.sessionSkills);
256
+ const builtInSkills = listBuiltInSkills(import.meta.dir);
257
+ const activeSkills = uniqueSkillRefs([...defaultSkills, ...profileSkills, ...agentSkills, ...sessionSkills]);
258
+ const knownSkills = uniqueSkillRefs([...builtInSkills, ...activeSkills]).sort((a, b) =>
259
+ skillDisplayName(a).localeCompare(skillDisplayName(b)),
260
+ );
261
+
262
+ return [
263
+ "Skills for current session",
264
+ "",
265
+ "Agent: " + agent.id,
266
+ "Session: " + sessionResult.session.id,
267
+ "",
268
+ "Available skills:",
269
+ ...formatSkillRows(knownSkills, { defaultSkills, profileSkills, agentSkills, sessionSkills, builtInSkills }),
270
+ "",
271
+ "Legend: ✅ active, ⬜ available but inactive",
272
+ "Load on the fly: /skills add <skill>",
273
+ "Unload: /skills remove <skill>",
274
+ "Clear session-loaded: /skills clear",
275
+ ].join("\n");
276
+ }
277
+
194
278
  private async modelResult(message: InboundMessage): Promise<BotCommandResult> {
195
279
  const sessionResult = await this.getDefaultSession(message);
196
280
  if (!sessionResult.ok) return { handled: true, text: sessionResult.message };
@@ -511,6 +595,34 @@ function formatDuration(ms: number): string {
511
595
  return Math.floor(hours / 24) + "d " + (hours % 24) + "h";
512
596
  }
513
597
 
598
+ function formatSkillRows(
599
+ skills: string[],
600
+ sources: { defaultSkills: string[]; profileSkills: string[]; agentSkills: string[]; sessionSkills: string[]; builtInSkills: string[] },
601
+ ): string[] {
602
+ if (skills.length === 0) return ["none"];
603
+
604
+ return skills.map((skill) => {
605
+ const name = skillDisplayName(skill);
606
+ const sourceLabels = [];
607
+
608
+ if (hasSkillRef(sources.defaultSkills, name)) sourceLabels.push("default");
609
+ if (hasSkillRef(sources.profileSkills, name)) sourceLabels.push("profile");
610
+ if (hasSkillRef(sources.agentSkills, name)) sourceLabels.push("agent");
611
+ if (hasSkillRef(sources.sessionSkills, name)) sourceLabels.push("session");
612
+ if (hasSkillRef(sources.builtInSkills, name)) sourceLabels.push("built-in");
613
+
614
+ const active = sourceLabels.some((source) => source !== "built-in");
615
+ const marker = active ? "✅" : "⬜";
616
+ const sourceText = sourceLabels.length > 0 ? sourceLabels.join(", ") : "configured";
617
+
618
+ return `${marker} ${name} (${active ? "active" : "inactive"}; ${sourceText})`;
619
+ });
620
+ }
621
+
622
+ function formatList(items: string[]): string {
623
+ return items.length > 0 ? items.join(", ") : "none";
624
+ }
625
+
514
626
  function formatPath(path: string): string {
515
627
  const home = process.env.HOME || process.env.USERPROFILE;
516
628
  if (home && path.startsWith(home)) {
@@ -0,0 +1,56 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ export const DEFAULT_ACTIVE_SKILLS = ["task-management"] as const;
5
+
6
+ export function defaultActiveSkills(): string[] {
7
+ return [...DEFAULT_ACTIVE_SKILLS];
8
+ }
9
+
10
+ export function parseSessionSkills(raw: string | null | undefined): string[] {
11
+ if (!raw) return [];
12
+
13
+ try {
14
+ const parsed = JSON.parse(raw);
15
+ if (!Array.isArray(parsed)) return [];
16
+
17
+ return parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
18
+ } catch {
19
+ return [];
20
+ }
21
+ }
22
+
23
+ export function skillDisplayName(ref: string): string {
24
+ const normalized = ref.replace(/\\/g, "/").replace(/\/$/, "");
25
+ return normalized.split("/").pop() || ref;
26
+ }
27
+
28
+ export function hasSkillRef(refs: string[], skillName: string): boolean {
29
+ return refs.some((ref) => ref === skillName || skillDisplayName(ref) === skillName);
30
+ }
31
+
32
+ export function uniqueSkillRefs(refs: string[]): string[] {
33
+ const seen = new Set<string>();
34
+ const unique: string[] = [];
35
+
36
+ for (const ref of refs.map((item) => item.trim()).filter(Boolean)) {
37
+ if (seen.has(ref)) continue;
38
+ seen.add(ref);
39
+ unique.push(ref);
40
+ }
41
+
42
+ return unique;
43
+ }
44
+
45
+ export function listBuiltInSkills(fromDir: string): string[] {
46
+ const skillsDir = resolve(fromDir, "../../skills");
47
+
48
+ if (!existsSync(skillsDir)) return [];
49
+
50
+ return readdirSync(skillsDir)
51
+ .filter((name) => {
52
+ const path = resolve(skillsDir, name);
53
+ return statSync(path).isDirectory() && existsSync(resolve(path, "SKILL.md"));
54
+ })
55
+ .sort();
56
+ }
@@ -181,6 +181,7 @@ function telegramCommands() {
181
181
  { command: "agents", description: "List agents available in this chat" },
182
182
  { command: "new", description: "Start a fresh chat session" },
183
183
  { command: "status", description: "Show current session status" },
184
+ { command: "skills", description: "List or load skills for this session" },
184
185
  { command: "model", description: "Choose model for this chat session" },
185
186
  { command: "thinking", description: "Choose thinking level for this chat session" },
186
187
  { command: "agent", description: "Send message to a specific agent" },
package/src/db/client.ts CHANGED
@@ -30,6 +30,7 @@ function ensureRuntimeSchema(): void {
30
30
  ensureColumn("agent_sessions", "active", "ALTER TABLE `agent_sessions` ADD `active` integer DEFAULT true NOT NULL");
31
31
  ensureColumn("agent_sessions", "ended_at", "ALTER TABLE `agent_sessions` ADD `ended_at` integer");
32
32
  ensureColumn("agent_sessions", "pi_session_path", "ALTER TABLE `agent_sessions` ADD `pi_session_path` text");
33
+ ensureColumn("agent_sessions", "session_skills", "ALTER TABLE `agent_sessions` ADD `session_skills` text DEFAULT '[]' NOT NULL");
33
34
  }
34
35
 
35
36
  function ensureColumn(table: string, column: string, statement: string): void {
@@ -75,6 +75,18 @@ export class SessionRepository {
75
75
  return this.requireById(id);
76
76
  }
77
77
 
78
+ async updateSessionSkills(id: string, skills: string[]): Promise<AgentSessionRow> {
79
+ await this.db
80
+ .update(agentSessions)
81
+ .set({
82
+ sessionSkills: JSON.stringify(skills),
83
+ updatedAt: Date.now(),
84
+ })
85
+ .where(eq(agentSessions.id, id));
86
+
87
+ return this.requireById(id);
88
+ }
89
+
78
90
  async findById(id: string): Promise<AgentSessionRow | null> {
79
91
  const row = await this.db.query.agentSessions.findFirst({
80
92
  where: eq(agentSessions.id, id),
package/src/db/schema.ts CHANGED
@@ -51,6 +51,7 @@ export const agentSessions = sqliteTable(
51
51
  instructionsHash: text("instructions_hash"),
52
52
  model: text("model"),
53
53
  thinkingLevel: text("thinking_level", { enum: ["off", "low", "medium", "high"] }),
54
+ sessionSkills: text("session_skills").notNull().default("[]"),
54
55
  active: integer("active", { mode: "boolean" }).notNull().default(true),
55
56
  endedAt: integer("ended_at"),
56
57
  createdAt: integer("created_at").notNull(),
@@ -10,6 +10,7 @@ import {
10
10
  import { mkdir } from "node:fs/promises";
11
11
  import { resolve } from "node:path";
12
12
  import type { LoadedAgentConfig, LoadedProfileConfig } from "../config/schemas";
13
+ import { defaultActiveSkills, parseSessionSkills } from "../config/skillRefs";
13
14
  import type { AgentSessionRow } from "../db/schema";
14
15
  import { resolveModelSelection } from "./PiModelResolver";
15
16
  import { loadOrCreatePiSession } from "./PiSessionFactory";
@@ -82,7 +83,7 @@ export class PiAgentRunner {
82
83
  compaction: { enabled: false },
83
84
  });
84
85
  const agentDir = getAgentDir();
85
- const skillPaths = resolveSkillPaths(input.agent, input.profile);
86
+ const skillPaths = resolveSkillPaths(input.agent, input.profile, input.session);
86
87
  const resourceLoader = new DefaultResourceLoader({
87
88
  cwd: workspace,
88
89
  agentDir,
@@ -114,9 +115,13 @@ export class PiAgentRunner {
114
115
  }
115
116
  }
116
117
 
117
- function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null): string[] {
118
+ function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfig | null, session: AgentSessionRow): string[] {
118
119
  const paths: string[] = [];
119
120
 
121
+ for (const s of defaultActiveSkills()) {
122
+ paths.push(resolveSkillPath(s, agent.baseDir));
123
+ }
124
+
120
125
  if (profile) {
121
126
  for (const s of profile.defaultSkills) {
122
127
  paths.push(resolveSkillPath(s, profile.baseDir));
@@ -127,6 +132,10 @@ function resolveSkillPaths(agent: LoadedAgentConfig, profile: LoadedProfileConfi
127
132
  paths.push(resolveSkillPath(s, agent.baseDir));
128
133
  }
129
134
 
135
+ for (const s of parseSessionSkills(session.sessionSkills)) {
136
+ paths.push(resolveSkillPath(s, agent.baseDir));
137
+ }
138
+
130
139
  return paths;
131
140
  }
132
141