@rubytech/taskmaster 1.0.105 → 1.0.107

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.
Files changed (40) hide show
  1. package/dist/agents/skills-status.js +23 -3
  2. package/dist/agents/skills.js +1 -0
  3. package/dist/agents/system-prompt.js +1 -0
  4. package/dist/agents/tools/memory-tool.js +2 -1
  5. package/dist/build-info.json +3 -3
  6. package/dist/config/zod-schema.js +12 -1
  7. package/dist/control-ui/assets/index-2XyxmiR6.css +1 -0
  8. package/dist/control-ui/assets/{index-DtuDNTAC.js → index-B_zHmTQU.js} +823 -645
  9. package/dist/control-ui/assets/index-B_zHmTQU.js.map +1 -0
  10. package/dist/control-ui/index.html +2 -2
  11. package/dist/control-ui/maxy-icon.png +0 -0
  12. package/dist/gateway/config-reload.js +1 -0
  13. package/dist/gateway/control-ui.js +111 -5
  14. package/dist/gateway/protocol/index.js +6 -1
  15. package/dist/gateway/protocol/schema/agents-models-skills.js +23 -0
  16. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -1
  17. package/dist/gateway/server-close.js +2 -0
  18. package/dist/gateway/server-http.js +6 -1
  19. package/dist/gateway/server-methods/brand.js +160 -0
  20. package/dist/gateway/server-methods/skills.js +159 -3
  21. package/dist/gateway/server-methods/wifi.js +0 -10
  22. package/dist/gateway/server-methods-list.js +5 -0
  23. package/dist/gateway/server-methods.js +2 -0
  24. package/dist/gateway/server-wifi-watchdog.js +105 -0
  25. package/dist/gateway/server.impl.js +3 -0
  26. package/dist/memory/manager.js +12 -3
  27. package/package.json +1 -1
  28. package/skills/skill-builder/SKILL.md +97 -0
  29. package/skills/skill-builder/references/lean-pattern.md +118 -0
  30. package/skills/zero-to-prototype/SKILL.md +35 -0
  31. package/skills/zero-to-prototype/references/discovery.md +64 -0
  32. package/skills/zero-to-prototype/references/prd.md +83 -0
  33. package/skills/zero-to-prototype/references/validation.md +67 -0
  34. package/taskmaster-docs/USER-GUIDE.md +58 -2
  35. package/templates/customer/agents/public/AGENTS.md +3 -10
  36. package/templates/taskmaster/agents/public/SOUL.md +0 -4
  37. package/templates/tradesupport/agents/public/AGENTS.md +3 -10
  38. package/dist/control-ui/assets/index-DjhCZlZd.css +0 -1
  39. package/dist/control-ui/assets/index-DtuDNTAC.js.map +0 -1
  40. package/skills/taskmaster/SKILL.md +0 -164
@@ -1,10 +1,13 @@
1
- import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveAgentWorkspaceDir, resolveAgentWorkspaceRoot, resolveDefaultAgentId } from "../../agents/agent-scope.js";
2
4
  import { installSkill } from "../../agents/skills-install.js";
3
5
  import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
4
- import { loadWorkspaceSkillEntries } from "../../agents/skills.js";
6
+ import { loadWorkspaceSkillEntries, resolveBundledSkillsDir } from "../../agents/skills.js";
7
+ import { bumpSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
5
8
  import { loadConfig, writeConfigFile } from "../../config/config.js";
6
9
  import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
7
- import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsInstallParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
10
+ import { ErrorCodes, errorShape, formatValidationErrors, validateSkillsBinsParams, validateSkillsCreateParams, validateSkillsDeleteDraftParams, validateSkillsDeleteParams, validateSkillsDraftsParams, validateSkillsInstallParams, validateSkillsReadParams, validateSkillsStatusParams, validateSkillsUpdateParams, } from "../protocol/index.js";
8
11
  function listWorkspaceDirs(cfg) {
9
12
  const dirs = new Set();
10
13
  const list = cfg.agents?.list;
@@ -45,6 +48,21 @@ function collectSkillBins(entries) {
45
48
  }
46
49
  return [...bins].sort();
47
50
  }
51
+ function resolveWorkspaceRoot(cfg) {
52
+ return resolveAgentWorkspaceRoot(cfg, resolveDefaultAgentId(cfg));
53
+ }
54
+ function isPreloadedSkill(skillKey) {
55
+ const dir = resolveBundledSkillsDir();
56
+ if (!dir)
57
+ return false;
58
+ try {
59
+ return fs.existsSync(`${dir}/${skillKey}`) &&
60
+ fs.statSync(`${dir}/${skillKey}`).isDirectory();
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ }
48
66
  export const skillsHandlers = {
49
67
  "skills.status": ({ params, respond }) => {
50
68
  if (!validateSkillsStatusParams(params)) {
@@ -97,6 +115,10 @@ export const skillsHandlers = {
97
115
  return;
98
116
  }
99
117
  const p = params;
118
+ if (p.enabled === false && isPreloadedSkill(p.skillKey)) {
119
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be disabled"));
120
+ return;
121
+ }
100
122
  const cfg = loadConfig();
101
123
  const skills = cfg.skills ? { ...cfg.skills } : {};
102
124
  const entries = skills.entries ? { ...skills.entries } : {};
@@ -134,4 +156,138 @@ export const skillsHandlers = {
134
156
  await writeConfigFile(nextConfig);
135
157
  respond(true, { ok: true, skillKey: p.skillKey, config: current }, undefined);
136
158
  },
159
+ "skills.read": ({ params, respond }) => {
160
+ if (!validateSkillsReadParams(params)) {
161
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.read params: ${formatValidationErrors(validateSkillsReadParams.errors)}`));
162
+ return;
163
+ }
164
+ const p = params;
165
+ const cfg = loadConfig();
166
+ const root = resolveWorkspaceRoot(cfg);
167
+ const skillDir = path.join(root, "skills", p.name);
168
+ const skillFile = path.join(skillDir, "SKILL.md");
169
+ if (!fs.existsSync(skillFile)) {
170
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
171
+ return;
172
+ }
173
+ const content = fs.readFileSync(skillFile, "utf-8");
174
+ const references = [];
175
+ const refsDir = path.join(skillDir, "references");
176
+ if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
177
+ for (const entry of fs.readdirSync(refsDir, { withFileTypes: true })) {
178
+ if (entry.isFile() && entry.name.endsWith(".md")) {
179
+ references.push({
180
+ name: entry.name,
181
+ content: fs.readFileSync(path.join(refsDir, entry.name), "utf-8"),
182
+ });
183
+ }
184
+ }
185
+ references.sort((a, b) => a.name.localeCompare(b.name));
186
+ }
187
+ respond(true, { name: p.name, content, references }, undefined);
188
+ },
189
+ "skills.create": async ({ params, respond }) => {
190
+ if (!validateSkillsCreateParams(params)) {
191
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.create params: ${formatValidationErrors(validateSkillsCreateParams.errors)}`));
192
+ return;
193
+ }
194
+ const p = params;
195
+ if (isPreloadedSkill(p.name)) {
196
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "cannot overwrite a preloaded skill"));
197
+ return;
198
+ }
199
+ const cfg = loadConfig();
200
+ const root = resolveWorkspaceRoot(cfg);
201
+ const skillDir = path.join(root, "skills", p.name);
202
+ // If the skill already exists (user skill), remove it first so we get a clean replace
203
+ if (fs.existsSync(skillDir)) {
204
+ fs.rmSync(skillDir, { recursive: true, force: true });
205
+ }
206
+ fs.mkdirSync(skillDir, { recursive: true });
207
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), p.skillContent, "utf-8");
208
+ if (p.references && p.references.length > 0) {
209
+ const refsDir = path.join(skillDir, "references");
210
+ fs.mkdirSync(refsDir, { recursive: true });
211
+ for (const ref of p.references) {
212
+ const safeName = ref.name.replace(/[^a-zA-Z0-9._-]/g, "-");
213
+ fs.writeFileSync(path.join(refsDir, safeName), ref.content, "utf-8");
214
+ }
215
+ }
216
+ bumpSkillsSnapshotVersion();
217
+ respond(true, { ok: true, name: p.name }, undefined);
218
+ },
219
+ "skills.delete": async ({ params, respond }) => {
220
+ if (!validateSkillsDeleteParams(params)) {
221
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.delete params: ${formatValidationErrors(validateSkillsDeleteParams.errors)}`));
222
+ return;
223
+ }
224
+ const p = params;
225
+ if (isPreloadedSkill(p.name)) {
226
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "preloaded skills cannot be deleted"));
227
+ return;
228
+ }
229
+ const cfg = loadConfig();
230
+ const root = resolveWorkspaceRoot(cfg);
231
+ const skillDir = path.join(root, "skills", p.name);
232
+ if (!fs.existsSync(skillDir)) {
233
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `skill "${p.name}" not found`));
234
+ return;
235
+ }
236
+ fs.rmSync(skillDir, { recursive: true, force: true });
237
+ bumpSkillsSnapshotVersion();
238
+ respond(true, { ok: true, name: p.name }, undefined);
239
+ },
240
+ "skills.drafts": ({ params, respond }) => {
241
+ if (!validateSkillsDraftsParams(params)) {
242
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.drafts params: ${formatValidationErrors(validateSkillsDraftsParams.errors)}`));
243
+ return;
244
+ }
245
+ const cfg = loadConfig();
246
+ const root = resolveWorkspaceRoot(cfg);
247
+ const draftsDir = path.join(root, ".skill-drafts");
248
+ if (!fs.existsSync(draftsDir)) {
249
+ respond(true, { drafts: [] }, undefined);
250
+ return;
251
+ }
252
+ const drafts = [];
253
+ for (const entry of fs.readdirSync(draftsDir, { withFileTypes: true })) {
254
+ if (!entry.isDirectory())
255
+ continue;
256
+ const skillFile = path.join(draftsDir, entry.name, "SKILL.md");
257
+ if (!fs.existsSync(skillFile))
258
+ continue;
259
+ const skillContent = fs.readFileSync(skillFile, "utf-8");
260
+ const references = [];
261
+ const refsDir = path.join(draftsDir, entry.name, "references");
262
+ if (fs.existsSync(refsDir) && fs.statSync(refsDir).isDirectory()) {
263
+ for (const ref of fs.readdirSync(refsDir, { withFileTypes: true })) {
264
+ if (ref.isFile() && ref.name.endsWith(".md")) {
265
+ references.push({
266
+ name: ref.name,
267
+ content: fs.readFileSync(path.join(refsDir, ref.name), "utf-8"),
268
+ });
269
+ }
270
+ }
271
+ references.sort((a, b) => a.name.localeCompare(b.name));
272
+ }
273
+ drafts.push({ name: entry.name, skillContent, references });
274
+ }
275
+ respond(true, { drafts }, undefined);
276
+ },
277
+ "skills.deleteDraft": ({ params, respond }) => {
278
+ if (!validateSkillsDeleteDraftParams(params)) {
279
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid skills.deleteDraft params: ${formatValidationErrors(validateSkillsDeleteDraftParams.errors)}`));
280
+ return;
281
+ }
282
+ const p = params;
283
+ const cfg = loadConfig();
284
+ const root = resolveWorkspaceRoot(cfg);
285
+ const draftDir = path.join(root, ".skill-drafts", p.name);
286
+ if (!fs.existsSync(draftDir)) {
287
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `draft "${p.name}" not found`));
288
+ return;
289
+ }
290
+ fs.rmSync(draftDir, { recursive: true, force: true });
291
+ respond(true, { ok: true, name: p.name }, undefined);
292
+ },
137
293
  };
@@ -227,16 +227,6 @@ export const wifiHandlers = {
227
227
  }
228
228
  // Check for saved connection profile
229
229
  const saved = await getSavedWifiConnection();
230
- // Auto-reconnect: if a saved profile with autoconnect exists but device
231
- // is disconnected, nudge NetworkManager to bring the connection up.
232
- // Fire-and-forget — the next status poll will reflect the result.
233
- if (saved && saved.autoconnect && !deviceState.connected) {
234
- runExec("nmcli", ["con", "up", saved.name], { timeoutMs: 30_000 })
235
- .then(() => disableWifiPowerSave(context.logGateway))
236
- .catch((err) => {
237
- context.logGateway.warn(`wifi auto-reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
238
- });
239
- }
240
230
  respond(true, {
241
231
  available: true,
242
232
  connected: deviceState.connected,
@@ -41,6 +41,11 @@ const BASE_METHODS = [
41
41
  "skills.bins",
42
42
  "skills.install",
43
43
  "skills.update",
44
+ "skills.read",
45
+ "skills.create",
46
+ "skills.delete",
47
+ "skills.drafts",
48
+ "skills.deleteDraft",
44
49
  "update.status",
45
50
  "update.run",
46
51
  "voicewake.get",
@@ -36,6 +36,7 @@ import { publicChatHandlers } from "./server-methods/public-chat.js";
36
36
  import { tailscaleHandlers } from "./server-methods/tailscale.js";
37
37
  import { wifiHandlers } from "./server-methods/wifi.js";
38
38
  import { workspacesHandlers } from "./server-methods/workspaces.js";
39
+ import { brandHandlers } from "./server-methods/brand.js";
39
40
  const ADMIN_SCOPE = "operator.admin";
40
41
  const READ_SCOPE = "operator.read";
41
42
  const WRITE_SCOPE = "operator.write";
@@ -236,6 +237,7 @@ export const coreGatewayHandlers = {
236
237
  ...memoryHandlers,
237
238
  ...recordsHandlers,
238
239
  ...workspacesHandlers,
240
+ ...brandHandlers,
239
241
  ...publicChatHandlers,
240
242
  ...tailscaleHandlers,
241
243
  ...wifiHandlers,
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Server-side WiFi watchdog for Raspberry Pi.
3
+ *
4
+ * Runs every 30 seconds on Linux. If a saved NetworkManager WiFi profile
5
+ * with autoconnect=yes exists but wlan0 is disconnected, nudges NM to
6
+ * reconnect and disables WiFi power save.
7
+ *
8
+ * This runs in the gateway process itself — independent of any UI polling —
9
+ * so WiFi recovers even when no browser is connected.
10
+ */
11
+ import os from "node:os";
12
+ import { runExec } from "../process/exec.js";
13
+ const WATCHDOG_INTERVAL_MS = 30_000;
14
+ let timer = null;
15
+ export function startWifiWatchdog(log) {
16
+ if (os.platform() !== "linux")
17
+ return;
18
+ if (timer)
19
+ return;
20
+ // Check immediately on startup, then every 30s
21
+ void runWifiCheck(log);
22
+ timer = setInterval(() => void runWifiCheck(log), WATCHDOG_INTERVAL_MS);
23
+ }
24
+ export function stopWifiWatchdog() {
25
+ if (timer) {
26
+ clearInterval(timer);
27
+ timer = null;
28
+ }
29
+ }
30
+ async function runWifiCheck(log) {
31
+ try {
32
+ // Verify nmcli is available
33
+ try {
34
+ await runExec("nmcli", ["--version"], { timeoutMs: 3_000 });
35
+ }
36
+ catch {
37
+ return; // nmcli not installed — nothing to do
38
+ }
39
+ // Check wlan0 device state
40
+ const { connected } = await getWlan0State();
41
+ if (connected)
42
+ return; // All good
43
+ // Check for a saved WiFi profile with autoconnect enabled
44
+ const saved = await getSavedAutoconnectProfile();
45
+ if (!saved)
46
+ return; // No saved profile — nothing to reconnect to
47
+ // Saved profile exists but WiFi is down — nudge NM to reconnect
48
+ log.info(`wifi watchdog: reconnecting to "${saved}"…`);
49
+ try {
50
+ await runExec("nmcli", ["con", "up", saved], { timeoutMs: 30_000 });
51
+ log.info(`wifi watchdog: reconnected to "${saved}"`);
52
+ // Disable power save to prevent future drops
53
+ try {
54
+ await runExec("iw", ["dev", "wlan0", "set", "power_save", "off"], { timeoutMs: 5_000 });
55
+ }
56
+ catch {
57
+ // Non-critical
58
+ }
59
+ }
60
+ catch (err) {
61
+ log.warn(`wifi watchdog: reconnect failed: ${err instanceof Error ? err.message : String(err)}`);
62
+ }
63
+ }
64
+ catch (err) {
65
+ log.warn(`wifi watchdog: check failed: ${err instanceof Error ? err.message : String(err)}`);
66
+ }
67
+ }
68
+ async function getWlan0State() {
69
+ try {
70
+ const { stdout } = await runExec("nmcli", ["-t", "-f", "GENERAL.STATE", "dev", "show", "wlan0"], { timeoutMs: 5_000 });
71
+ for (const line of stdout.split("\n")) {
72
+ if (line.startsWith("GENERAL.STATE:")) {
73
+ return { connected: line.includes("100") };
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ // wlan0 might not exist
79
+ }
80
+ return { connected: false };
81
+ }
82
+ async function getSavedAutoconnectProfile() {
83
+ try {
84
+ const { stdout } = await runExec("nmcli", ["-t", "-f", "NAME,TYPE,AUTOCONNECT", "con", "show"], { timeoutMs: 5_000 });
85
+ for (const line of stdout.split("\n")) {
86
+ if (!line.trim())
87
+ continue;
88
+ const placeholder = "\x00";
89
+ const safe = line.replace(/\\:/g, placeholder);
90
+ const parts = safe.split(":");
91
+ if (parts.length < 3)
92
+ continue;
93
+ const name = parts[0].replace(new RegExp(placeholder, "g"), ":").trim();
94
+ const type = parts[1].trim();
95
+ const autoconnect = parts[2].trim().toLowerCase() === "yes";
96
+ if (type === "802-11-wireless" && autoconnect) {
97
+ return name;
98
+ }
99
+ }
100
+ }
101
+ catch {
102
+ // Non-critical
103
+ }
104
+ return null;
105
+ }
@@ -50,6 +50,7 @@ import { startGatewaySidecars } from "./server-startup.js";
50
50
  import { logGatewayStartup } from "./server-startup-log.js";
51
51
  import { ensureWatchdogUnitOnStartup, scheduleWatchdogStabilityConfirmation, } from "./server-watchdog.js";
52
52
  import { startGatewayTailscaleExposure } from "./server-tailscale.js";
53
+ import { startWifiWatchdog } from "./server-wifi-watchdog.js";
53
54
  import { loadGatewayTlsRuntime } from "./server/tls.js";
54
55
  import { createWizardSessionTracker } from "./server-wizard-sessions.js";
55
56
  import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
@@ -421,6 +422,8 @@ export async function startGatewayServer(port = 18789, opts = {}) {
421
422
  logChannels,
422
423
  logBrowser,
423
424
  }));
425
+ // Start WiFi watchdog on Linux — auto-reconnects saved WiFi if connection drops
426
+ startWifiWatchdog(log.child("wifi"));
424
427
  // Initialize memory managers for all agents and sync indexes at gateway startup
425
428
  const logMemory = log.child("memory");
426
429
  const agentIds = listAgentIds(cfgAtStart);
@@ -74,6 +74,15 @@ function expandPathTemplate(pattern, ctx) {
74
74
  result = result.replaceAll("{agentId}", ctx.agentId);
75
75
  return result;
76
76
  }
77
+ /**
78
+ * Ensure phone numbers in memory/users/ paths include the canonical + prefix.
79
+ * AI agents sometimes omit the + when constructing paths (e.g. "memory/users/447734875155/...")
80
+ * but the session peer and filesystem convention always use + (e.g. "+447734875155").
81
+ * Without this, scope patterns like memory/users/{peer}/** won't match the path.
82
+ */
83
+ function normalizePhoneInMemoryPath(relPath) {
84
+ return relPath.replace(/^(memory\/users\/)(\d)/i, "$1+$2");
85
+ }
77
86
  /**
78
87
  * Simple glob pattern matcher supporting * and **.
79
88
  * - * matches any characters except /
@@ -517,7 +526,7 @@ export class MemoryIndexManager {
517
526
  return this.syncing;
518
527
  }
519
528
  async readFile(params) {
520
- const relPath = normalizeRelPath(params.relPath);
529
+ const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
521
530
  if (!relPath || !isMemoryPath(relPath)) {
522
531
  throw new Error("path required");
523
532
  }
@@ -552,7 +561,7 @@ export class MemoryIndexManager {
552
561
  * matching the session's scope configuration.
553
562
  */
554
563
  async writeFile(params) {
555
- const relPath = normalizeRelPath(params.relPath);
564
+ const relPath = normalizePhoneInMemoryPath(normalizeRelPath(params.relPath));
556
565
  if (!relPath || !isMemoryPath(relPath)) {
557
566
  throw new Error("path required (must be in memory/ directory)");
558
567
  }
@@ -598,7 +607,7 @@ export class MemoryIndexManager {
598
607
  let relPath;
599
608
  if (params.destFolder) {
600
609
  // Explicit folder — use as-is (scope checking enforces access)
601
- relPath = `${params.destFolder}/${params.destFilename}`;
610
+ relPath = normalizePhoneInMemoryPath(`${params.destFolder}/${params.destFilename}`);
602
611
  }
603
612
  else {
604
613
  // Default: memory/users/{peer}/media/{filename}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/taskmaster",
3
- "version": "1.0.105",
3
+ "version": "1.0.107",
4
4
  "description": "AI-powered business assistant for small businesses",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: skill-builder
3
+ description: "Guide the user through creating a new lean skill — collect name, description, behaviour rules, and references, then save as a draft for the user to install via the Control Panel."
4
+ user-invocable: true
5
+ disable-model-invocation: true
6
+ ---
7
+
8
+ # Skill Builder
9
+
10
+ A deterministic walkthrough for creating new lean skills. Follow each step in order. Be conversational — one question at a time.
11
+
12
+ ## When to Activate
13
+
14
+ The admin requests a new skill, asks to "create a skill", "add a skill", or invokes `/skill-builder`.
15
+
16
+ ## Before Starting
17
+
18
+ Load `references/lean-pattern.md` for the template and examples. Use it throughout as your reference for correct structure.
19
+
20
+ ## Step 1: Understand the Purpose
21
+
22
+ Ask: **"What should this skill do?"**
23
+
24
+ Listen for:
25
+ - What capability it adds (scheduling, quoting, inventory, etc.)
26
+ - When it should activate (what triggers it)
27
+ - What tools or data it needs (memory, web search, file access, etc.)
28
+
29
+ Clarify until you can describe the skill in one sentence. This becomes the `description` in frontmatter.
30
+
31
+ ## Step 2: Choose a Name
32
+
33
+ Propose a name based on the purpose. Names must be:
34
+ - Lowercase
35
+ - Hyphen-separated (e.g. `inventory-management`)
36
+ - Short and descriptive
37
+
38
+ Confirm with the user.
39
+
40
+ ## Step 3: Define Behaviour
41
+
42
+ Ask what rules the agent should follow when using this skill:
43
+ - **Tone and style** — formal, casual, WhatsApp-short?
44
+ - **Activation conditions** — when exactly should the agent use this skill?
45
+ - **Hard boundaries** — what should the agent never do?
46
+ - **Data sources** — where does the skill's knowledge live? (memory, external API, files)
47
+ - **Escalation** — when should it hand off to admin?
48
+
49
+ Not every skill needs all of these. Only include what's relevant.
50
+
51
+ ## Step 4: Decide on References
52
+
53
+ If the skill has detailed procedures, templates, or data formats, those belong in `references/` files — not inline in SKILL.md.
54
+
55
+ Ask: **"Are there any detailed procedures, templates, or formats this skill needs?"**
56
+
57
+ For each reference file:
58
+ - Give it a descriptive filename (e.g. `event-format.md`, `quoting-rules.md`)
59
+ - Collect the content from the user — they can dictate, paste, or describe it and you draft it
60
+
61
+ If the skill is simple enough to fit in SKILL.md alone (under ~30 lines of instructions), skip references.
62
+
63
+ ## Step 5: Compose the Skill
64
+
65
+ Using the lean pattern from `references/lean-pattern.md`, compose:
66
+
67
+ 1. **SKILL.md** — frontmatter (`name`, `description`) + activation conditions + behaviour rules + references index
68
+ 2. **Reference files** — one per detailed topic
69
+
70
+ Show the user the complete SKILL.md content and each reference file. Ask them to review.
71
+
72
+ ## Step 6: Save the Draft
73
+
74
+ Use your `write` tool to save the skill as a draft:
75
+
76
+ 1. Create `../../.skill-drafts/{name}/SKILL.md` with the composed content
77
+ 2. For each reference file: create `../../.skill-drafts/{name}/references/{filename}`
78
+
79
+ The `../../` resolves from your agent directory to the workspace root. The `.skill-drafts/` folder at the workspace root is where the Control Panel looks for drafts.
80
+
81
+ **Important:** Write to `.skill-drafts/`, NOT directly to `skills/`. The user installs the skill through the Control Panel.
82
+
83
+ ## Step 7: Direct to Control Panel
84
+
85
+ Tell the user:
86
+
87
+ > "Your skill draft is ready. To install it:
88
+ > 1. Open the **Control Panel** → **Advanced** → **Skills**
89
+ > 2. Click **Add Skill**
90
+ > 3. Your draft will appear under 'Import from draft' — click it to load
91
+ > 4. Review the content and click **Save Skill**
92
+ >
93
+ > Once saved, the skill will be available to agents on the next message."
94
+
95
+ ---
96
+
97
+ **Remember:** Be conversational. Don't dump all questions at once. Guide through each step one at a time.
@@ -0,0 +1,118 @@
1
+ # Lean SKILL.md Pattern
2
+
3
+ Skills teach agents specialised capabilities. Each skill is a directory containing a `SKILL.md` file and optional `references/` subdirectory.
4
+
5
+ ## Directory Structure
6
+
7
+ ```
8
+ skills/
9
+ my-skill/
10
+ SKILL.md ← Main file (lean index)
11
+ references/
12
+ detailed-guide.md ← Loaded on demand via skill_read
13
+ templates.md
14
+ ```
15
+
16
+ ## SKILL.md Template
17
+
18
+ ```markdown
19
+ ---
20
+ name: my-skill
21
+ description: "One-line summary of what this skill does — used for agent routing."
22
+ ---
23
+
24
+ # My Skill
25
+
26
+ Applies when [activation conditions].
27
+
28
+ ## When to Activate
29
+
30
+ - [Trigger condition 1]
31
+ - [Trigger condition 2]
32
+
33
+ ## Behaviour
34
+
35
+ [Concise rules — keep this section short. Move detailed procedures to references.]
36
+
37
+ ## References
38
+
39
+ Load `references/detailed-guide.md` for [what it covers].
40
+ Load `references/templates.md` for [what it covers].
41
+ ```
42
+
43
+ ## Principles
44
+
45
+ 1. **SKILL.md is an index, not a manual.** Keep it under 30 lines of instructions. The agent reads it on every activation — bloated skills waste context.
46
+
47
+ 2. **References hold the detail.** Procedures, templates, data formats, and examples go in `references/`. The agent loads them on demand with `skill_read`.
48
+
49
+ 3. **Frontmatter is required.** Every SKILL.md must start with YAML frontmatter containing `name` and `description`. Missing `description` causes the skill to be silently skipped.
50
+
51
+ 4. **Description drives routing.** The description is injected into the agent's prompt as part of the available skills list. Make it specific enough that the agent activates the skill for the right messages.
52
+
53
+ 5. **Behaviour, not data.** Skills define how the agent should act. Business-specific data (pricing, customer info, hours) belongs in memory, not in the skill file. Skills are generic; memory is specific.
54
+
55
+ ## Optional Frontmatter Fields
56
+
57
+ ```yaml
58
+ metadata: {"taskmaster":{"always":true,"emoji":"📦","primaryEnv":"MY_API_KEY"}}
59
+ ```
60
+
61
+ | Field | Purpose |
62
+ |-------|---------|
63
+ | `always` | Always include in prompt (skip eligibility checks) |
64
+ | `emoji` | Display emoji in the Control Panel |
65
+ | `primaryEnv` | Env var for API key — enables the key input field in the UI |
66
+ | `requires.bins` | Required binaries (e.g. `["curl"]`) |
67
+ | `requires.env` | Required environment variables |
68
+
69
+ Most user-created skills need only `name` and `description`.
70
+
71
+ ## Examples
72
+
73
+ ### Simple skill (no references)
74
+
75
+ ```markdown
76
+ ---
77
+ name: weather
78
+ description: "Provide weather context for scheduling and outdoor work decisions."
79
+ ---
80
+
81
+ # Weather
82
+
83
+ When scheduling or planning outdoor work, check weather conditions.
84
+
85
+ ## When to Activate
86
+
87
+ - Customer asks about weather
88
+ - Scheduling outdoor appointments
89
+ - Weather might affect planned work
90
+
91
+ ## Behaviour
92
+
93
+ Use the `web_search` tool to check current weather for the business location.
94
+ Keep weather info brief — one or two sentences unless asked for detail.
95
+ ```
96
+
97
+ ### Lean skill with references
98
+
99
+ ```markdown
100
+ ---
101
+ name: event-management
102
+ description: "Manage anything time-bound: appointments, meetings, reminders, follow-ups."
103
+ ---
104
+
105
+ # Event Management
106
+
107
+ Applies when handling anything time-bound.
108
+
109
+ ## When to Activate
110
+
111
+ - Customer requests or confirms an appointment
112
+ - Business owner asks to schedule, reschedule, or cancel something
113
+ - A reminder or follow-up needs to be recorded
114
+
115
+ ## References
116
+
117
+ Load `references/events.md` for the standard event template, file naming, and calendar query instructions.
118
+ ```
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: zero-to-prototype
3
+ description: "Guide business owners from a raw idea to a validated concept — customer discovery, assumption testing, value proposition, and a simple PRD. For entrepreneurs exploring new products, services, or pivots."
4
+ metadata: {"taskmaster":{"emoji":"🚀"}}
5
+ ---
6
+
7
+ # Zero to Prototype
8
+
9
+ Helps business owners validate ideas before investing time and money. Covers the full journey from initial concept to a clear product requirement document.
10
+
11
+ ## When to Activate
12
+
13
+ - Owner says they have an idea for a new product, service, or offering
14
+ - Owner wants to validate whether something is worth pursuing
15
+ - Owner asks about customer discovery, market validation, or testing assumptions
16
+ - Owner wants to write a PRD or product brief
17
+ - Phrases like "pressure test this idea", "is this worth building", "help me think through this"
18
+
19
+ ## Workflow
20
+
21
+ Walk through each phase conversationally. Don't dump all phases at once — progress naturally based on where the owner is.
22
+
23
+ **Phase 1 — Capture the idea.** Understand what they want to build and why. Load `references/discovery.md`.
24
+
25
+ **Phase 2 — Validate assumptions.** Identify and test the riskiest assumptions. Load `references/validation.md`.
26
+
27
+ **Phase 3 — Define the product.** Turn validated concepts into a clear PRD. Load `references/prd.md`.
28
+
29
+ ## References
30
+
31
+ Load reference files on demand as each phase is reached — not all at once.
32
+
33
+ - `references/discovery.md` — customer discovery framework, interview questions, market signals
34
+ - `references/validation.md` — assumption mapping, validation techniques, go/no-go criteria
35
+ - `references/prd.md` — PRD template, prioritisation, scope definition